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:
2026-10-21 20:25:15 -04:00
co-authored by Claude Opus 5.5
parent a0c3921edd
commit e7e527197f
20 changed files with 1780 additions and 60 deletions
+158 -30
View File
@@ -474,6 +474,10 @@ function getPassageKey(book, chapter, verse, verseEnd, translation, mode = 'bibl
const tabId = getActiveTabId();
return tabId ? `maps-annotations-${tabId}` : 'maps-annotations';
}
if (mode === 'browser') {
const tabId = getActiveTabId();
return tabId ? `browser-annotations-${tabId}` : 'browser-annotations';
}
if (mode === 'notes') {
const tabId = getActiveTabId();
return `notes-annotations-${tabId}`;
@@ -2046,8 +2050,22 @@ function linkifyScriptureRefs(escapedHtml) {
} catch (_) { return escapedHtml; }
}
/*
* The Browser tab's pages live in one persistent layer inside .tab-content (#browserTabHost, owned by
* browser-tab.js). Moving or re-creating a <webview> reloads its page — the search and the scroll would be
* lost on every tab switch — so clearing the panel area removes everything EXCEPT that layer.
*/
function clearTabContentKeepingBrowser(tabContent) {
if (!tabContent) return;
Array.from(tabContent.childNodes).forEach((n) => {
if (n.id !== 'browserTabHost') tabContent.removeChild(n);
});
}
// Open a passage in a brand-new Bible tab (used by tapped references; mirrors the remote's go-to).
function openPassageInNewTab(book, chapter, verse, verseEnd) {
// `translation` (optional) is honoured only when it is one the operator has switched on — a reference
// tapped on a web page's NIV card opens in NIV, so the room reads the same words it just saw.
function openPassageInNewTab(book, chapter, verse, verseEnd, translation) {
try {
const app = window.bibleApp;
if (!app || !book || !chapter) return;
@@ -2058,6 +2076,12 @@ function openPassageInNewTab(book, chapter, verse, verseEnd) {
tab.chapter = parseInt(chapter, 10) || 1;
tab.verse = parseInt(verse, 10) || 1;
tab.verseEnd = verseEnd ? (parseInt(verseEnd, 10) || null) : null;
if (translation) {
try {
const code = String(translation).toUpperCase();
if (getVisibleTranslationCodes().includes(code)) tab.translation = code;
} catch (_) {}
}
tab.loaded = false;
// Scroll to the tapped verse once the chapter renders (the chapter opens AT the verse,
// not at the top).
@@ -2833,14 +2857,17 @@ export default class BibleApp {
}
const tabContent = document.querySelector('.tab-content');
if (tabContent) tabContent.innerHTML = '';
if (tabContent) clearTabContentKeepingBrowser(tabContent);
this.tabs = [];
this.activeTabId = null;
// Recreate default tabs (announcements, graphics, whiteboard, notes, prayer, ask, bible)
// Reset puts the Browser tab back on its start page as well (browser-tab.js keeps it per session).
try { if (window.__browserTab) window.__browserTab.resetSession(); } catch (_) {}
// Recreate default tabs (announcements, graphics, maps, browser, whiteboard, notes, prayer, ask, bible)
runAutomaticOperation(() => {
this.createAnnouncementsTab();
this.createGraphicsTab();
this.createMapsTab();
this.createBrowserTab();
this.createWhiteboardTab();
this.createNotesTab();
this.createPrayerTab();
@@ -3514,6 +3541,7 @@ export default class BibleApp {
// Create initial tabs automatically without recording them as user operations:
// 1) Announcements tab (leftmost; image-first view)
// 2) Graphics tab (image carousel from Graphics/ directory)
// 2b) Maps, then Browser (a live web page — loads now so it is ready when first opened)
// 3) Whiteboard tab (not active)
// 4) Notes tab
// 5) Prayer tab
@@ -3523,6 +3551,7 @@ export default class BibleApp {
this.createAnnouncementsTab();
this.createGraphicsTab();
this.createMapsTab();
this.createBrowserTab();
this.createWhiteboardTab();
this.createNotesTab();
this.createPrayerTab();
@@ -5266,6 +5295,29 @@ export default class BibleApp {
this._insertGeneralTab(tab);
this.renderTabs();
}
// Browser tab: a live web page, full-bleed in the content area with no browser chrome. The page itself
// is owned by browser-tab.js (it loads at startup and keeps running while other tabs are showing).
createBrowserTab() {
// Sort directly after Maps (-14) and before Whiteboard (-10)
const tabId = this.generateUniqueTabId(Date.now() - 13);
const tab = {
id: tabId,
book: '',
chapter: null,
verse: null,
verseEnd: null,
translation: 'ESV',
loaded: true,
mode: 'browser',
mediaContent: null,
mediaTitle: 'Browser',
browserLabel: '', // the page's search term or title (browser-tab.js) — OSC info line, phone remote
isSelectingVerses: false,
verseSelectionCooldown: 0
};
this._insertGeneralTab(tab);
this.renderTabs();
}
createWhiteboardTab() {
if (!isAutomaticOperation) {
pulseCompanionActionFeedback('new_whiteboard_tab');
@@ -5545,8 +5597,12 @@ export default class BibleApp {
// });
}
// Create draggable popup keyboard
createPrayerKeyboard(keyboardEl, textEl, tabId) {
// Create draggable popup keyboard.
// `sink` (optional) types somewhere other than a contenteditable — the Browser tab passes one that types
// into its web page: { insertText(text), key('Backspace'|'Enter'|'Tab'), focus() }. With a sink there is
// no formatting toolbar, and keys never take focus, so the page's field keeps its caret between taps.
createPrayerKeyboard(keyboardEl, textEl, tabId, sink = null) {
const kbSlot = sink ? '_browserKbHandlers' : '_prayerKbHandlers'; // separate drag listeners per keyboard
// Traditional QWERTY layout: Tab leads the Q-row, Shift leads the Z-row,
// Backspace tails the Z-row, and Space + Enter share the action row —
// matching a real keyboard so muscle memory works.
@@ -5592,6 +5648,7 @@ export default class BibleApp {
// Build keyboard HTML with formatting toolbar
let keyboardHTML = '<div class="prayer-keyboard-header">';
keyboardHTML += '<span class="prayer-keyboard-title">Keyboard</span>';
if (!sink) {
keyboardHTML += '<div class="prayer-format-toolbar">';
keyboardHTML += '<button class="prayer-format-btn" data-format="bold" title="Bold (Ctrl+B)"><b>B</b></button>';
keyboardHTML += '<button class="prayer-format-btn" data-format="italic" title="Italic (Ctrl+I)"><i>I</i></button>';
@@ -5607,6 +5664,7 @@ export default class BibleApp {
// Paste icon (clipboard)
keyboardHTML += '<button class="prayer-format-btn" data-format="paste" title="Paste (Ctrl+V)"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg></button>';
keyboardHTML += '</div>';
}
keyboardHTML += `<button class="prayer-keyboard-close" onclick="this.closest('.prayer-keyboard').style.display='none'" aria-label="Close keyboard">${CLOSE_GLYPH}</button>`;
keyboardHTML += '</div>';
keyboardHTML += '<div class="prayer-keyboard-body">';
@@ -5701,12 +5759,12 @@ export default class BibleApp {
// Remove any previously-registered prayer-keyboard document listeners so they don't
// stack on every re-render (the keyboard DOM is rebuilt each time the prayer tab renders).
if (this._prayerKbHandlers) {
document.removeEventListener('mousemove', this._prayerKbHandlers.mousemove);
document.removeEventListener('touchmove', this._prayerKbHandlers.touchmove);
document.removeEventListener('mouseup', this._prayerKbHandlers.mouseup);
document.removeEventListener('touchend', this._prayerKbHandlers.touchend);
if (this._prayerKbHandlers.selectionchange) document.removeEventListener('selectionchange', this._prayerKbHandlers.selectionchange);
if (this[kbSlot]) {
document.removeEventListener('mousemove', this[kbSlot].mousemove);
document.removeEventListener('touchmove', this[kbSlot].touchmove);
document.removeEventListener('mouseup', this[kbSlot].mouseup);
document.removeEventListener('touchend', this[kbSlot].touchend);
if (this[kbSlot].selectionchange) document.removeEventListener('selectionchange', this[kbSlot].selectionchange);
}
const onPrayerMouseMove = (e) => {
if (isDragging) {
@@ -5727,7 +5785,7 @@ export default class BibleApp {
document.addEventListener('touchmove', onPrayerTouchMove);
document.addEventListener('mouseup', onPrayerMouseUp);
document.addEventListener('touchend', onPrayerTouchEnd);
this._prayerKbHandlers = { mousemove: onPrayerMouseMove, touchmove: onPrayerTouchMove, mouseup: onPrayerMouseUp, touchend: onPrayerTouchEnd };
this[kbSlot] = { mousemove: onPrayerMouseMove, touchmove: onPrayerTouchMove, mouseup: onPrayerMouseUp, touchend: onPrayerTouchEnd };
// Three-state shift: 'off' | 'on' | 'locked' (caps lock).
// 'off' -> letters lowercase, numbers as-is
@@ -5812,11 +5870,11 @@ export default class BibleApp {
btn.addEventListener('click', () => {
const key = btn.dataset.key;
if (key === 'backspace') {
document.execCommand('delete', false);
if (sink) sink.key('Backspace'); else document.execCommand('delete', false);
} else if (key === 'enter') {
insertTextInto('\n');
if (sink) sink.key('Enter'); else insertTextInto('\n');
} else if (key === 'tab') {
insertTextInto('\t');
if (sink) sink.key('Tab'); else insertTextInto('\t');
} else if (key === 'shift') {
const now = Date.now();
if (shiftState === 'locked') {
@@ -5839,15 +5897,27 @@ export default class BibleApp {
} else if (/[0-9]/.test(key) && numberShiftActive() && btn.dataset.shift) {
textToInsert = btn.dataset.shift;
}
insertTextInto(textToInsert);
if (sink) sink.insertText(textToInsert); else insertTextInto(textToInsert);
// Auto-release momentary Shift after any character key — but never auto-release Caps Lock.
if (shiftState === 'on') {
setShiftState('off');
}
}
textEl.focus();
if (sink) sink.focus(); else textEl.focus();
});
// With a sink, a key must never take focus: the page's field would lose its caret and the next
// key would type into nothing.
if (sink) {
btn.addEventListener('pointerdown', (e) => { e.preventDefault(); });
btn.addEventListener('mousedown', (e) => { e.preventDefault(); });
}
});
if (sink) {
// Taps on the keyboard's own background must not take focus either. The header's drag handler
// already prevents its own mousedown, so dragging is unaffected.
keyboardEl.addEventListener('mousedown', (e) => { e.preventDefault(); });
return; // the rest is the Prayer tab's formatting toolbar
}
// Handle formatting button clicks
keyboardEl.querySelectorAll('.prayer-format-btn').forEach(btn => {
@@ -5958,7 +6028,7 @@ export default class BibleApp {
}
};
document.addEventListener('selectionchange', onPrayerSelectionChange);
if (this._prayerKbHandlers) this._prayerKbHandlers.selectionchange = onPrayerSelectionChange;
if (this[kbSlot]) this[kbSlot].selectionchange = onPrayerSelectionChange;
}
// Keyboard shortcuts for formatting and copy/paste
@@ -5993,6 +6063,7 @@ export default class BibleApp {
if (parsed.ask === undefined) parsed.ask = true;
if (parsed.graphics === undefined) parsed.graphics = true;
if (parsed.maps === undefined) parsed.maps = true;
if (parsed.browser === undefined) parsed.browser = true;
if (parsed.ppsync === undefined) parsed.ppsync = true;
}
return parsed;
@@ -6006,6 +6077,7 @@ export default class BibleApp {
announcements: true,
graphics: true,
maps: true,
browser: true,
whiteboard: true,
notes: true,
prayer: true,
@@ -6173,6 +6245,15 @@ export default class BibleApp {
<rect x="2" y="3" width="20" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="1.6"/>
<text x="12" y="16" text-anchor="middle" font-size="14" font-family="Georgia, serif" fill="currentColor">M</text>
</svg>`;
} else if (tab.mode === 'browser') {
// A globe in the same rounded frame as the other display tabs.
tabElement.title = 'Browser' + (tab.browserLabel ? ' \u2014 ' + tab.browserLabel : '');
iconHTML = `<svg class="browser-tab-icon" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false">
<rect x="2" y="3" width="20" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="1.6"/>
<circle cx="12" cy="12" r="5.6" fill="none" stroke="currentColor" stroke-width="1.3"/>
<ellipse cx="12" cy="12" rx="2.4" ry="5.6" fill="none" stroke="currentColor" stroke-width="1.1"/>
<line x1="6.4" y1="12" x2="17.6" y2="12" stroke="currentColor" stroke-width="1.1"/>
</svg>`;
} else if (tab.mode === 'prayer') {
iconHTML = `<svg class="prayer-tab-icon" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false">
<rect x="2" y="3" width="20" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="1.6"/>
@@ -7085,13 +7166,15 @@ export default class BibleApp {
// The row just changed shape, so "tab 2 of 5" may have too (OSC information line).
try { scheduleCompanionActiveTabFeedback(); } catch (_) {}
// Show or hide the Browser tab's page to match (and give new Browser tabs a page, closed ones none).
try { if (window.__browserTab) window.__browserTab.sync(); } catch (_) {}
}
toggleTabMode(tabId) {
const tab = this.tabs.find(t => t.id === tabId);
if (!tab) return;
// Prevent toggling mode for fixed-mode tabs (whiteboard/notes/alpha/announcements/graphics/prayer/ask)
if (tab.mode === 'whiteboard' || tab.mode === 'notes' || tab.mode === 'alpha' || tab.mode === 'announcements' || tab.mode === 'graphics' || tab.mode === 'maps' || tab.mode === 'prayer' || tab.mode === 'ask' || tab.ppsync) {
// Prevent toggling mode for fixed-mode tabs (whiteboard/notes/alpha/announcements/graphics/maps/browser/prayer/ask)
if (tab.mode === 'whiteboard' || tab.mode === 'notes' || tab.mode === 'alpha' || tab.mode === 'announcements' || tab.mode === 'graphics' || tab.mode === 'maps' || tab.mode === 'browser' || tab.mode === 'prayer' || tab.mode === 'ask' || tab.ppsync) {
console.log('[MODE SWITCH] Fixed-mode tab; ignoring toggle for tab:', tabId);
return;
}
@@ -8982,8 +9065,8 @@ export default class BibleApp {
}
});
} catch (_) {}
} else if (tab.mode === 'announcements' || tab.mode === 'graphics' || tab.mode === 'maps') {
// Announcements/Graphics/Maps: tools otherwise unchanged (highlighter reset above).
} else if (tab.mode === 'announcements' || tab.mode === 'graphics' || tab.mode === 'maps' || tab.mode === 'browser') {
// Announcements/Graphics/Maps/Browser: tools otherwise unchanged (highlighter reset above).
try {
runAutomaticOperation(() => {
if (typeof updateToolUI === 'function') {
@@ -9279,7 +9362,7 @@ export default class BibleApp {
const key = [tab.book, tab.chapter, tab.verse, tab.verseEnd, tab.translation].join('|');
const bt = tab._panelEl.querySelector(`#bibleText${tab.id}`);
if (tab._panelKey === key && bt && bt.textContent && bt.textContent.trim().length > 0) {
tabContent.innerHTML = '';
clearTabContentKeepingBrowser(tabContent);
tab._panelEl.classList.remove('tab-fade-enter', 'tab-fade-enter-active');
tab._panelEl.classList.add('active');
tabContent.appendChild(tab._panelEl);
@@ -9323,7 +9406,7 @@ export default class BibleApp {
}
} catch (_) {}
}
tabContent.innerHTML = '';
clearTabContentKeepingBrowser(tabContent);
const panelElement = document.createElement('div');
// Start hidden to allow a quick fade-in when switching tabs
panelElement.className = 'tab-panel active tab-fade-enter';
@@ -9862,6 +9945,13 @@ export default class BibleApp {
initializeCarousel();
try { ensureContentResizeObserver(tab.id); } catch (_) {}
}, 50); // Small delay to ensure DOM is ready
} else if (tab.mode === 'browser') {
// Browser: the page is NOT in this panel. It lives in browser-tab.js's persistent layer over the
// content area, because rebuilding it here on every switch would reload it. The panel is just a
// black reading area, so everything that looks up readingArea<id> for the active tab finds one.
panelElement.innerHTML = `
<div class="reading-area browser-reading-area" id="readingArea${tab.id}" style="display:block;background:#000;overflow:hidden;padding:0;margin:0;"></div>`;
setTimeout(() => { try { if (window.__browserTab) window.__browserTab.sync(); } catch (_) {} }, 0);
} else if (tab.mode === 'graphics' || tab.mode === 'maps') {
// Graphics & Maps: identical image-first swipeable carousel; only the data source differs
// (/api/graphics/list + /Graphics/ vs /api/maps/list + /Maps/). Mirrors the Announcements pattern.
@@ -14897,7 +14987,7 @@ function sendCompanionPenColorStateFeedback(color, force = false) {
*/
const COMPANION_TAB_KIND = {
bible: 'Bible', whiteboard: 'Whiteboard', notes: 'Notes', media: 'Media', alpha: 'Alpha',
announcements: 'Announcements', graphics: 'Graphics', maps: 'Maps', prayer: 'Prayer', ask: 'Ask'
announcements: 'Announcements', graphics: 'Graphics', maps: 'Maps', browser: 'Browser', prayer: 'Prayer', ask: 'Ask'
};
let companionActiveTabSignature = null;
let companionActiveTabTimer = 0;
@@ -14917,8 +15007,9 @@ function sendCompanionActiveTabFeedback() {
// The same thing again as a stable lowercase slug. kind_ is a label for the information line and
// could be reworded; `slug` is what an OSC path is built from, so it must never drift.
slug: tab.ppsync ? 'sync' : String(tab.mode || 'tab').toLowerCase(),
passage: (tab.book && tab.chapter) ? `${tab.book} ${tab.chapter}` : null,
translation: tab.translation || null
// The Browser tab has no passage; its search term (or page title) answers "what is up there" instead.
passage: tab.mode === 'browser' ? (tab.browserLabel || null) : ((tab.book && tab.chapter) ? `${tab.book} ${tab.chapter}` : null),
translation: tab.mode === 'browser' ? null : (tab.translation || null)
};
const sig = JSON.stringify(payload);
if (sig === companionActiveTabSignature) return;
@@ -16950,6 +17041,7 @@ document.addEventListener('DOMContentLoaded', () => {
{ mode: 'announcements', label: 'Announcements', icon: 'A' },
{ mode: 'graphics', label: 'Graphics', icon: 'G' },
{ mode: 'maps', label: 'Maps', icon: 'M' },
{ mode: 'browser', label: 'Browser', icon: '\u{1F310}' },
{ mode: 'whiteboard', label: 'Whiteboard', icon: 'WB' },
{ mode: 'notes', label: 'Notes', icon: 'N' },
{ mode: 'prayer', label: 'Prayer', icon: 'P' },
@@ -16973,6 +17065,7 @@ document.addEventListener('DOMContentLoaded', () => {
announcements: true,
graphics: true,
maps: true,
browser: true,
whiteboard: true,
notes: true,
prayer: true,
@@ -17009,6 +17102,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (mode === 'announcements' && typeof app.createAnnouncementsTab === 'function') app.createAnnouncementsTab();
else if (mode === 'graphics' && typeof app.createGraphicsTab === 'function') app.createGraphicsTab();
else if (mode === 'maps' && typeof app.createMapsTab === 'function') app.createMapsTab();
else if (mode === 'browser' && typeof app.createBrowserTab === 'function') app.createBrowserTab();
else if (mode === 'whiteboard' && typeof app.createWhiteboardTab === 'function') app.createWhiteboardTab();
else if (mode === 'notes' && typeof app.createNotesTab === 'function') app.createNotesTab();
else if (mode === 'prayer' && typeof app.createPrayerTab === 'function') app.createPrayerTab();
@@ -17364,6 +17458,8 @@ try {
*/
try {
const tabId = mirrorTabId();
// The Browser tab scrolls its web page, with the same gesture contract (browser-tab.js).
if (tabId && window.__browserTab && window.__browserTab.isBrowserTab(tabId)) { window.__browserTab.mirrorScroll(tabId, payload); break; }
const readingArea = tabId ? document.getElementById(`readingArea${tabId}`) : null;
if (!readingArea) break;
if (payload.t !== undefined) {
@@ -17439,18 +17535,23 @@ try {
// The phone remote's reading-navigation buttons. These call exactly what the on-screen
// chapter-header buttons call, so scrolling from the phone behaves identically.
case 'scroll_top': {
// Browser tab: its page (the ±5-verse buttons move it a screenful). Every other tab: the header nav.
try { if (window.__browserTab && window.__browserTab.scroll(window.bibleApp.activeTabId, { edge: 'top', smooth: true })) break; } catch (_) {}
try { if (window.__headerNav) window.__headerNav.top(window.bibleApp.activeTabId); } catch (_) {}
break;
}
case 'scroll_bottom': {
try { if (window.__browserTab && window.__browserTab.scroll(window.bibleApp.activeTabId, { edge: 'bottom', smooth: true })) break; } catch (_) {}
try { if (window.__headerNav) window.__headerNav.bottom(window.bibleApp.activeTabId); } catch (_) {}
break;
}
case 'scroll_up_5': {
try { if (window.__browserTab && window.__browserTab.scroll(window.bibleApp.activeTabId, { pages: -1, smooth: true })) break; } catch (_) {}
try { if (window.__headerNav) window.__headerNav.by(-5, window.bibleApp.activeTabId); } catch (_) {}
break;
}
case 'scroll_down_5': {
try { if (window.__browserTab && window.__browserTab.scroll(window.bibleApp.activeTabId, { pages: 1, smooth: true })) break; } catch (_) {}
try { if (window.__headerNav) window.__headerNav.by(5, window.bibleApp.activeTabId); } catch (_) {}
break;
}
@@ -17658,6 +17759,11 @@ try {
localStorage.setItem(key, str);
switch (key) {
case 'browserTabUrl':
case 'browserTabZoom':
case 'browserTabScriptureTaps':
try { if (window.__browserTab) window.__browserTab.applySettings(key); } catch (_) {}
break;
case 'tabVisibility':
// Re-render the tab bar; renderTabs() also moves off a tab it just hid.
if (window.bibleApp && typeof window.bibleApp.renderTabs === 'function') window.bibleApp.renderTabs();
@@ -17780,6 +17886,24 @@ try {
} catch (_) {}
break;
}
// OSC /bibleapp/browser — same as /bibleapp/maps: bring the (first) Browser tab up.
case 'showBrowser': {
try {
const app = window.bibleApp;
const t = app && app.tabs && app.tabs.find(x => x.mode === 'browser');
if (t && typeof app.switchTab === 'function') app.switchTab(t.id);
} catch (_) {}
break;
}
// Recovery for a live web page in front of the room: reload it, or take it back to its start page.
case 'browserReload': {
try { if (window.__browserTab) window.__browserTab.reload(); } catch (_) {}
break;
}
case 'browserHome': {
try { if (window.__browserTab) window.__browserTab.home(); } catch (_) {}
break;
}
case 'switchToMode': {
try {
const app = window.bibleApp;
@@ -17841,6 +17965,9 @@ try {
const x = px * window.innerWidth, y = py * window.innerHeight;
const ov = document.querySelector('.splash-overlay');
if (ov && !ov.classList.contains('hidden')) { try { window.hideSplash(); } catch (_) {} break; }
// Over the Browser tab's page, elementFromPoint finds only the <webview>; the tap is replayed
// inside the page instead (a button, the search box, a scripture reference).
if (window.__browserTab && window.__browserTab.tapAt(x, y)) break;
const el = document.elementFromPoint(x, y);
if (!el) break;
const opts = { clientX: x, clientY: y, bubbles: true, cancelable: true, view: window };
@@ -22762,7 +22889,7 @@ function getOrCreateAnnotationCanvas(tabId) {
// Check if this is a media, announcements, prayer, or ask tab - if so, don't create annotation canvases
const tab = window.bibleApp?.tabs?.find(t => t.id === tabId);
if (tab && (tab.mode === 'media' || tab.mode === 'announcements' || tab.mode === 'graphics' || tab.mode === 'maps' || tab.mode === 'prayer' || tab.mode === 'ask')) {
if (tab && (tab.mode === 'media' || tab.mode === 'announcements' || tab.mode === 'graphics' || tab.mode === 'maps' || tab.mode === 'browser' || tab.mode === 'prayer' || tab.mode === 'ask')) {
console.log('[DEBUG] Skipping annotation canvas creation for', tab.mode, 'tab:', tabId);
return null;
}
@@ -23516,7 +23643,7 @@ function setupAnnotationForActiveTab() {
console.log('[DEBUG] *** setupAnnotationForActiveTab called, tabId:', tabId, 'drawing state:', drawing);
if (!tabId) return;
const tab = getTabById(tabId);
if (tab && (tab.mode === 'announcements' || tab.mode === 'graphics' || tab.mode === 'maps' || tab.mode === 'prayer' || tab.mode === 'ask')) {
if (tab && (tab.mode === 'announcements' || tab.mode === 'graphics' || tab.mode === 'maps' || tab.mode === 'browser' || tab.mode === 'prayer' || tab.mode === 'ask')) {
console.log('[DEBUG] Skipping annotation setup for', tab.mode, 'tab');
return;
}
@@ -23994,7 +24121,7 @@ function startDrawing(e) {
const tabId = getActiveTabId();
const activeTab = getTabById(tabId);
if (activeTab && (activeTab.mode === 'announcements' || activeTab.mode === 'graphics' || activeTab.mode === 'maps')) {
if (activeTab && (activeTab.mode === 'announcements' || activeTab.mode === 'graphics' || activeTab.mode === 'maps' || activeTab.mode === 'browser')) {
console.log('[DEBUG] Drawing blocked on', activeTab.mode, 'tab');
return;
}
@@ -24711,6 +24838,7 @@ function getPassageKeyForTabId(tabId) {
if (tab.mode === 'announcements') return `announcements-annotations-${tabId}`;
if (tab.mode === 'graphics') return `graphics-annotations-${tabId}`;
if (tab.mode === 'maps') return `maps-annotations-${tabId}`;
if (tab.mode === 'browser') return `browser-annotations-${tabId}`;
if (tab.mode === 'notes') return `notes-annotations-${tabId}`;
return getPassageKey(tab.book, tab.chapter, tab.verse, tab.verseEnd, tab.translation, tab.mode, !!tab.ppsync);
}