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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user