book-reader/app/src/main/assets/epub_reader.js
Aryan 49e08cc9f1
Epub improvements (#162)
* Added a "Remove Edge Padding" option in "Visual Options"

* Added support for paragraph gap settings in the EPUB vertical reader.

* Added support for adjustable paragraph gaps in the paginated reader.

* fix(epub-pagination): ensure custom fonts override embedded epub fonts

* feat(epub): allow overlapping highlights and resolve gesture selection conflicts
2026-04-10 17:43:01 +05:30

2653 lines
97 KiB
JavaScript

// epub_reader.js
(function () {
function applyMobileOptimizationsAndSelection() {
var viewport = document.querySelector("meta[name=viewport]");
if (!viewport) {
viewport = document.createElement("meta");
viewport.setAttribute("name", "viewport");
document.head.appendChild(viewport);
}
viewport.setAttribute("content", "width=device-width, initial-scale=1.0, maximum-scale=1.5, user-scalable=yes");
var style = document.getElementById("customMobileStyle");
if (!style) {
style = document.createElement("style");
style.setAttribute("id", "customMobileStyle");
document.head.appendChild(style);
}
// CHANGED: Colors now use rgba() with 0.5 opacity for blending
style.innerHTML = ` html {
margin: 0; padding: 0; height: 100%; overflow-y: scroll;
}
body {
margin: 0; padding: 0px !important;
word-wrap: break-word; overflow-wrap: break-word;
word-break: break-word; -webkit-text-size-adjust: 100%;
text-rendering: optimizeLegibility; -webkit-user-select: none;
-moz-user-select: none; -ms-user-select: none; user-select: none;
scroll-behavior: auto !important;
}
#content-container {
will-change: transform;
transition: none !important;
}
p, div, li, td, th, span {
font-size: 1em; line-height: inherit;
}
p, ul, ol {
margin-top: 0.5em; margin-bottom: 0.5em;
}
li {
margin-bottom: 0.25em;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.3; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.75em;
}
h3 {
font-size: 1.5em;
}
h4 {
font-size: 1.25em;
}
h5 {
font-size: 1.1em;
}
h6 {
font-size: 1em;
}
img, svg, video, canvas {
max-width: 100%; width: 100%; height: auto; display: block; margin-left: auto; margin-right: auto; background-color: transparent; object-fit: contain;
}
figure img {
height: auto !important;
}
div:has(> img:only-child), p:has(> img:only-child) {
height: auto !important; line-height: normal !important;
}
.tts-highlight {
background-color: rgba(255, 236, 179, 0.8);
/* Semi-transparent Gold */
color: black !important;
padding: 0.1em 0;
border-radius: 3px;
}
html.dark-theme span.tts-highlight {
background-color: rgba(160, 140, 90, 0.8) !important;
color: #E0E0E0 !important;
}
/* User Highlights */
.user-highlight-yellow {
background-color: rgba(251, 192, 45, 0.4); cursor: pointer;
}
.user-highlight-green {
background-color: rgba(56, 142, 60, 0.4); cursor: pointer;
}
.user-highlight-blue {
background-color: rgba(25, 118, 210, 0.4); cursor: pointer;
}
.user-highlight-red {
background-color: rgba(211, 47, 47, 0.4); cursor: pointer;
}
.user-highlight-purple {
background-color: rgba(123, 31, 162, 0.4); cursor: pointer;
}
.user-highlight-orange {
background-color: rgba(245, 124, 0, 0.4); cursor: pointer;
}
.user-highlight-cyan {
background-color: rgba(0, 151, 167, 0.4); cursor: pointer;
}
.user-highlight-magenta {
background-color: rgba(194, 24, 91, 0.4); cursor: pointer;
}
.user-highlight-lime {
background-color: rgba(175, 180, 43, 0.4); cursor: pointer;
}
.user-highlight-pink {
background-color: rgba(233, 30, 99, 0.4); cursor: pointer;
}
.user-highlight-teal {
background-color: rgba(0, 121, 107, 0.4); cursor: pointer;
}
.user-highlight-indigo {
background-color: rgba(48, 63, 159, 0.4); cursor: pointer;
}
.user-highlight-black {
background-color: rgba(0, 0, 0, 0.4); cursor: pointer;
}
.user-highlight-white {
background-color: rgba(255, 255, 255, 0.4); cursor: pointer;
/* Optional: slight border so white is visible on white paper */
border-bottom: 1px solid rgba(0,0,0,0.1);
}
/* Active State (Darkens slightly when pressed) */
span[class^="user-highlight-"]:active {
filter: brightness(0.9);
}
mark.search-highlight {
background-color: rgba(160, 207, 241, 0.8);
color: black;
border-radius: 3px;
}
html.dark-theme mark.search-highlight {
background-color: rgba(0, 90, 156, 0.8);
color: #E0E0E0;
}
`;
window.setTextSelectionEnabled = function (enabled) {
var selectStyle = enabled ? "auto" : "none";
if (document.body) {
document.body.style.webkitUserSelect = selectStyle;
document.body.style.mozUserSelect = selectStyle;
document.body.style.msUserSelect = selectStyle;
document.body.style.userSelect = selectStyle;
}
};
window.setTextSelectionEnabled(true);
}
window.VIEWPORT_PADDING_TOP = 0;
window.VIEWPORT_PADDING_BOTTOM = 0;
window.setViewportPadding = function (top, bottom) {
window.VIEWPORT_PADDING_TOP = top || 0;
window.VIEWPORT_PADDING_BOTTOM = bottom || 0;
};
window.applyReaderTheme = function (isDark, bgHex, textHex, textureBase64) {
var styleId = "readerThemeStyle";
var themeStyleElement = document.getElementById(styleId);
if (!themeStyleElement) {
themeStyleElement = document.createElement("style");
themeStyleElement.setAttribute("id", styleId);
document.head.appendChild(themeStyleElement);
}
var themeClassName = isDark ? "dark-theme" : "light-theme";
var oppositeThemeClassName = isDark ? "light-theme" : "dark-theme";
document.documentElement.classList.remove(oppositeThemeClassName);
document.documentElement.classList.add(themeClassName);
var effectiveBg = bgHex || (isDark ? '#121212' : '#FFFFFF');
var effectiveText = textHex || (isDark ? '#E0E0E0' : '#000000');
var textureCss = textureBase64
? `background-image: url('${textureBase64}'); background-repeat: repeat; background-blend-mode: multiply;`
: 'background-image: none;';
var css = `
:root {
--reader-bg: ${effectiveBg};
--reader-text: ${effectiveText};
}
html.${themeClassName}, html.${themeClassName} body {
background-color: var(--reader-bg) !important;
color: var(--reader-text) !important;
${textureCss}
}
html.${themeClassName} a {
color: ${isDark ? '#BB86FC' : '#1A0DAB'} !important;
}
html.${themeClassName} a p,
html.${themeClassName} a div,
html.${themeClassName} a span,
html.${themeClassName} a li,
html.${themeClassName} a h1,
html.${themeClassName} a h2,
html.${themeClassName} a h3,
html.${themeClassName} a h4,
html.${themeClassName} a h5,
html.${themeClassName} a h6 {
color: var(--reader-text) !important;
background-color: transparent !important;
}
html.${themeClassName} blockquote, html.${themeClassName} pre,
html.${themeClassName} figcaption, html.${themeClassName} caption,
html.${themeClassName} label, html.${themeClassName} dt, html.${themeClassName} dd {
color: inherit !important;
background-color: transparent !important;
}
html.${themeClassName} hr {
border-color: ${isDark ? '#444444' : '#CCCCCC'} !important;
background-color: ${isDark ? '#444444' : '#CCCCCC'} !important;
}
html.${themeClassName} table, html.${themeClassName} tr, html.${themeClassName} td, html.${themeClassName} th {
background-color: transparent !important;
border-color: ${isDark ? '#555' : '#CCC'} !important;
}
`;
themeStyleElement.innerHTML = css;
if (window.adjustInlineColorsForContrast) {
window.adjustInlineColorsForContrast(isDark, effectiveBg);
}
};
function getLuminance(r, g, b) {
var a = [r, g, b].map(function (v) {
v /= 255;
return v <= 0.03928
? v / 12.92
: Math.pow((v + 0.055) / 1.055, 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : {r:255,g:255,b:255};
}
function rgbStringToRgb(rgbStr) {
var parts = rgbStr.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
if (parts) {
return { r: parseInt(parts[1]), g: parseInt(parts[2]), b: parseInt(parts[3]) };
}
return null;
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return[h, s, l];
}
function hslToRgb(h, s, l) {
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return[Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
window.adjustInlineColorsForContrast = function(isDark, bgHex) {
var bgRgb = hexToRgb(bgHex);
var bgLum = getLuminance(bgRgb.r, bgRgb.g, bgRgb.b);
var elements = document.querySelectorAll('[style*="color"]');
elements.forEach(function(el) {
var style = window.getComputedStyle(el);
var colorStr = style.color;
var rgb = rgbStringToRgb(colorStr);
if (rgb) {
var lum = getLuminance(rgb.r, rgb.g, rgb.b);
var l1 = Math.max(bgLum, lum);
var l2 = Math.min(bgLum, lum);
var contrast = (l1 + 0.05) / (l2 + 0.05);
if (contrast < 4.5) {
var hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
if (bgLum < 0.5) {
hsl[2] = Math.max(hsl[2], 0.7); // Lighten
} else {
hsl[2] = Math.min(hsl[2], 0.3); // Darken
}
var newRgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
el.style.setProperty('color', `rgb(${newRgb[0]}, ${newRgb[1]}, ${newRgb[2]})`, 'important');
}
}
});
var bgElements = document.querySelectorAll('[style*="background"]');
bgElements.forEach(function(el) {
var style = window.getComputedStyle(el);
var bgStr = style.backgroundColor;
if (bgStr && bgStr !== 'rgba(0, 0, 0, 0)' && bgStr !== 'transparent') {
var rgb = rgbStringToRgb(bgStr);
if (rgb) {
var lum = getLuminance(rgb.r, rgb.g, rgb.b);
if (isDark && lum > 0.5) {
el.style.setProperty('background-color', 'transparent', 'important');
} else if (!isDark && lum < 0.2) {
el.style.setProperty('background-color', 'transparent', 'important');
}
}
}
});
};
function handleHighlightInteraction(e) {
if (window.getSelection && window.getSelection().toString().trim().length > 0) {
return false;
}
var target = e.target;
var highlightSpan = null;
while (target && target !== document.body) {
var isHighlight = false;
if (target.nodeType === Node.ELEMENT_NODE && target.classList) {
for (var i = 0; i < target.classList.length; i++) {
if (target.classList[i].startsWith("user-highlight-")) {
isHighlight = true;
break;
}
}
}
if (isHighlight) {
highlightSpan = target;
break;
}
target = target.parentNode;
}
if (highlightSpan) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (window.getSelection) {
window.getSelection().removeAllRanges();
}
var text = highlightSpan.textContent;
var rect = highlightSpan.getBoundingClientRect();
var rawCfi = highlightSpan.getAttribute("data-cfi");
if (!rawCfi) {
var cfiResult = window.getCfiPathForElement(highlightSpan, 0);
rawCfi = cfiResult.cfi;
}
var cfiToReport = rawCfi;
if (rawCfi && rawCfi.includes(";;")) {
var cfiParts = rawCfi.split(";;");
cfiToReport = cfiParts[cfiParts.length - 1];
}
if (window.HighlightBridge) {
window.HighlightBridge.onHighlightClicked(cfiToReport, text, rect.left, rect.top, rect.right, rect.bottom);
}
return true;
}
return false;
}
// 1. Handle Taps (Click)
document.addEventListener(
"click",
function (e) {
handleHighlightInteraction(e);
},
true
);
window.updateReaderStyles = function (fontSizeEm, lineHeight, fontFamily, textAlign, paragraphGap) {
var logTag = "ReaderFontDiagnosis";
console.log(
logTag +
": updateReaderStyles called. Size: " +
fontSizeEm +
", LineHeight: " +
lineHeight +
", Font: '" +
fontFamily +
"', Align: '" +
textAlign +
"', Gap: " +
paragraphGap
);
var dynamicStyleId = "dynamicReaderStyles";
var dynamicStyleElement = document.getElementById(dynamicStyleId);
if (!dynamicStyleElement) {
dynamicStyleElement = document.createElement("style");
dynamicStyleElement.setAttribute("id", dynamicStyleId);
document.head.appendChild(dynamicStyleElement);
}
var newFontSize = parseFloat(fontSizeEm);
var newLineHeight = parseFloat(lineHeight);
var newGap = parseFloat(paragraphGap);
if (isNaN(newFontSize) || newFontSize < 0.5 || newFontSize > 5.0) newFontSize = 1.0;
if (isNaN(newLineHeight) || newLineHeight < 1.0 || newLineHeight > 3.0) newLineHeight = 1.6;
if (isNaN(newGap) || newGap < 0.0 || newGap > 3.0) newGap = 1.0;
var fontCss = "";
var selector = "body";
if (fontFamily && fontFamily !== "Original" && fontFamily !== "") {
var fallback = "sans-serif";
if (fontFamily === "Merriweather" || fontFamily === "Lora") {
fallback = "serif";
} else if (fontFamily === "Roboto Mono") {
fallback = "monospace";
}
selector = "body, p, span, div, li, a, h1, h2, h3, h4, h5, h6, blockquote, td, th";
fontCss = "font-family: '" + fontFamily + "', " + fallback + " !important;";
}
// --- ALIGNMENT LOGIC ---
var alignCss = "";
var alignSelector = "body, p, li, div, h1, h2, h3, h4, h5, h6";
if (textAlign === "left") {
alignCss =
` ` +
alignSelector +
` {
text-align: left !important;
}
`;
} else if (textAlign === "justify") {
alignCss =
` ` +
alignSelector +
` {
text-align: justify !important;
-webkit-hyphens: auto !important;
hyphens: auto !important;
}
`;
}
// --- GAP LOGIC ---
var gapCss = `
body p, body ul, body ol, body blockquote {
margin-top: ` + (0.5 * newGap) + `em !important;
margin-bottom: ` + (0.5 * newGap) + `em !important;
}
body li {
margin-bottom: ` + (0.25 * newGap) + `em !important;
}
`;
dynamicStyleElement.innerHTML =
` body {
font-size: ` +
newFontSize +
`em !important;
line-height: ` +
newLineHeight +
` !important;
}
` +
selector +
` {
` +
fontCss +
`
}
` +
alignCss +
gapCss;
setTimeout(
function () {
var computedBody = window.getComputedStyle(document.body).fontFamily;
console.log(logTag + ": [BODY] Computed font-family: " + computedBody);
var firstPara = document.querySelector("p");
if (firstPara) {
var computedPara = window.getComputedStyle(firstPara).fontFamily;
var computedAlign = window.getComputedStyle(firstPara).textAlign;
console.log(logTag + ": [PARAGRAPH] Computed font-family: " + computedPara);
console.log(logTag + ": [PARAGRAPH] Inner Text Sample: " + firstPara.innerText.substring(0, 20));
} else {
console.log(logTag + ": [PARAGRAPH] No <p> tag found to check.");
}
if (fontFamily && fontFamily !== "") {
var isCheckAvailable = document.fonts && document.fonts.check;
if (isCheckAvailable) {
var loaded = document.fonts.check("12px '" + fontFamily + "'");
console.log(logTag + ": Font Loading Status -> document.fonts.check('" + fontFamily + "') = " + loaded);
} else {
console.log(logTag + ": document.fonts API not available.");
}
}
},
300,
);
if (window.triggerInitialScrollStateReport) {
window.triggerInitialScrollStateReport();
} else if (window.reportScrollState) {
setTimeout(window.reportScrollState, 60);
}
};
window.TOC_FRAGMENTS = window.TOC_FRAGMENTS || [];
window.setTocFragments = function (jsonArray) {
console.log("FRAG_NAV_DEBUG: window.setTocFragments called with " + jsonArray.length + " items.");
window.TOC_FRAGMENTS = jsonArray;
// Immediate audit and report
window.auditTocFragments();
window.reportScrollState();
};
window.reportScrollState = function () {
if (typeof PageInfoReporter !== "undefined" && PageInfoReporter.updateScrollState) {
var scrollY = Math.round(window.scrollY || window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0);
var scrollHeight = Math.round(Math.max(document.body.scrollHeight, document.documentElement.scrollHeight));
var clientHeight = Math.round(document.documentElement.clientHeight || window.innerHeight || 0);
if (clientHeight === 0) return;
var activeFragment = null;
var hasFoundAnyElementInDom = false;
if (window.TOC_FRAGMENTS && window.TOC_FRAGMENTS.length > 0) {
// Adjust threshold to be slightly more forgiving (padding + 60px)
var threshold = window.VIEWPORT_PADDING_TOP + 60;
for (var i = 0; i < window.TOC_FRAGMENTS.length; i++) {
var id = window.TOC_FRAGMENTS[i];
// FIX: Look for both 'id' and 'name' attributes
var el = document.getElementById(id) || document.querySelector('[name="' + id + '"]');
if (el) {
hasFoundVisible = true;
var rect = el.getBoundingClientRect();
// Log individual element positions so we can see them in your FRAG_NAV_DEBUG filter
console.log("FRAG_NAV_DEBUG: Checking #" + id + " | rect.top: " + Math.round(rect.top) + " | threshold: " + threshold);
if (rect.top <= threshold) {
activeFragment = id;
} else {
break;
}
} else {
if (!hasFoundVisible) {
activeFragment = id;
}
}
}
}
// Fallback for the very start of the chapter
if (activeFragment === null && scrollY < 50 && window.TOC_FRAGMENTS && window.TOC_FRAGMENTS.length > 0) {
activeFragment = window.TOC_FRAGMENTS[0];
}
PageInfoReporter.updateScrollState(scrollY, scrollHeight, clientHeight, activeFragment);
}
window.reportTopChunk();
};
// ADD THIS NEW DIAGNOSTIC FUNCTION
window.auditTocFragments = function () {
console.log("FRAG_NAV_DEBUG: --- Starting DOM Audit ---");
if (!window.TOC_FRAGMENTS || window.TOC_FRAGMENTS.length === 0) {
console.log("FRAG_NAV_DEBUG: Audit failed - window.TOC_FRAGMENTS is empty.");
return;
}
var foundCount = 0;
window.TOC_FRAGMENTS.forEach((id) => {
var el = document.getElementById(id);
if (el) {
foundCount++;
console.log("FRAG_NAV_DEBUG: [FOUND] ID: " + id + " | Tag: " + el.tagName + " | OffsetTop: " + el.offsetTop);
} else {
console.log("FRAG_NAV_DEBUG: [MISSING] ID: " + id + " - Not in DOM.");
}
});
console.log("FRAG_NAV_DEBUG: Audit complete. Found " + foundCount + "/" + window.TOC_FRAGMENTS.length);
// Also log some random IDs from the DOM to see what's actually there
var allWithId = document.querySelectorAll("[id]");
console.log(
"FRAG_NAV_DEBUG: Sample IDs existing in DOM: " +
Array.from(allWithId)
.slice(0, 5)
.map((el) => el.id)
.join(", "),
);
};
window.addEventListener("scroll", window.reportScrollState, { passive: true });
window.addEventListener("resize", window.reportScrollState);
window.triggerInitialScrollStateReport = function () {
var attempts = 0;
var maxAttempts = 15;
function tryReport() {
attempts++;
if (window.reportScrollState) {
window.reportScrollState();
}
if (attempts < maxAttempts) {
setTimeout(tryReport, 200);
}
}
setTimeout(tryReport, 50);
};
window.scrollToChapterStart = function () {
requestAnimationFrame(function () {
window.scrollTo(0, 0);
setTimeout(
function () {
window.reportScrollState();
},
100,
);
});
};
window.scrollToChapterEnd = function () {
requestAnimationFrame(function () {
var targetScrollY =
(document.body.scrollHeight || document.documentElement.scrollHeight) - (window.innerHeight || document.documentElement.clientHeight);
if (targetScrollY < 0) targetScrollY = 0;
window.scrollTo(0, targetScrollY);
setTimeout(
function () {
window.reportScrollState();
},
100,
);
});
};
window.scrollToSpecificY = function (yPosition) {
if (typeof yPosition === "number" && yPosition >= 0) {
window.scrollTo(0, yPosition);
setTimeout(
function () {
window.reportScrollState();
},
100,
);
} else {
setTimeout(
function () {
window.reportScrollState();
},
100,
);
}
};
function initializeReaderContent() {
applyMobileOptimizationsAndSelection();
}
if (document.readyState === "complete" || document.readyState === "interactive") {
initializeReaderContent();
} else {
document.addEventListener("DOMContentLoaded", initializeReaderContent);
}
window.CURRENT_SEARCH_QUERY = "";
window.clearSearchHighlights = function () {
window.CURRENT_SEARCH_QUERY = "";
document.querySelectorAll("mark.search-highlight").forEach(function (el) {
var parent = el.parentNode;
if (parent) {
while (el.firstChild) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
parent.normalize();
}
});
return "JS: Search highlights cleared.";
};
window.highlightAllOccurrences = function (query) {
window.clearSearchHighlights();
window.CURRENT_SEARCH_QUERY = query;
if (!query || query.length < 2) return "JS: Query too short for highlighting.";
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
var nodesToModify =[];
while ((node = walker.nextNode())) {
if (node.nodeValue.toLowerCase().includes(query.toLowerCase())) {
nodesToModify.push(node);
}
}
var regex = new RegExp("(" + query.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&") + ")", "gi");
nodesToModify.forEach(function (textNode) {
if (textNode.parentNode && textNode.parentNode.tagName !== "SCRIPT" && textNode.parentNode.tagName !== "STYLE") {
var tempDiv = document.createElement("div");
tempDiv.innerHTML = textNode.nodeValue.replace(regex, '<mark class="search-highlight">$1</mark>');
var parent = textNode.parentNode;
while (tempDiv.firstChild) {
parent.insertBefore(tempDiv.firstChild, textNode);
}
parent.removeChild(textNode);
}
});
return "JS: Highlighted " + document.querySelectorAll("mark.search-highlight").length + " occurrences.";
};
window.scrollToChunkOccurrence = function (chunkIndex, relativeIndex) {
console.log("NavDiag: scrollToChunkOccurrence chunk=" + chunkIndex + ", relativeIdx=" + relativeIndex);
var chunkDiv = document.querySelector(`.chunk-container[data-chunk-index='${chunkIndex}']`);
if (chunkDiv) {
let wasEmpty = false;
if (chunkDiv.innerHTML === "" && window.virtualization && window.virtualization.chunksData[chunkIndex]) {
console.log("NavDiag: Chunk was empty, restoring content before scrolling.");
chunkDiv.innerHTML = window.virtualization.chunksData[chunkIndex];
chunkDiv.style.height = "";
wasEmpty = true;
}
var highlights = chunkDiv.querySelectorAll("mark.search-highlight");
if ((wasEmpty || highlights.length === 0) && window.CURRENT_SEARCH_QUERY) {
window.highlightAllOccurrences(window.CURRENT_SEARCH_QUERY);
highlights = chunkDiv.querySelectorAll("mark.search-highlight");
}
if (highlights && highlights.length > 0) {
var targetIdx = (relativeIndex >= 0 && relativeIndex < highlights.length) ? relativeIndex : 0;
highlights[targetIdx].scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" });
return "JS: Scrolled to relative occurrence " + targetIdx + " in chunk " + chunkIndex;
} else {
chunkDiv.scrollIntoView({ behavior: "auto", block: "center" });
return "JS: No highlights in chunk, scrolled to chunk center.";
}
}
return "JS: Chunk " + chunkIndex + " not found.";
};
window.removeHighlight = function () {
var highlightNode;
var removedCount = 0;
while ((highlightNode = document.querySelector(".tts-highlight")) !== null) {
var parent = highlightNode.parentNode;
if (parent) {
try {
while (highlightNode.firstChild) {
parent.insertBefore(highlightNode.firstChild, highlightNode);
}
parent.removeChild(highlightNode);
parent.normalize();
removedCount++;
} catch (e) {
if (highlightNode.parentNode) {
// Check if it wasn't already removed by a concurrent process
highlightNode.remove(); // Fallback removal
}
break; // Exit loop on error to prevent infinite loop on a problematic node
}
} else {
try {
highlightNode.remove();
removedCount++;
} catch (e_orphan) {
// console.error("JS: Error removing orphaned highlight node: " + e_orphan.message, highlightNode);
}
break;
}
}
};
const TTS_HIGHLIGHT_LOG_TAG = "TTS_HIGHLIGHT_DIAGNOSIS";
window.highlightFromCfi = function (cfi, textToHighlight, startOffset) {
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: highlightFromCfi called. CFI='${cfi}', Offset=$ {
startOffset
}
, Text='${textToHighlight.substring(0, 50)}...' `);
console.log("TTS_LIST_DIAG: Attempting highlight. CFI: " + cfi + " Text: '" + textToHighlight.substring(0, 20) + "' Offset: " + startOffset);
window.removeHighlight();
if (!cfi || !textToHighlight) {
const errorMsg = "JS: CFI or text missing.";
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: $ {
errorMsg
}
`);
return errorMsg;
}
try {
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: Resolving CFI to node...`);
const location = window.getNodeAndOffsetFromCfi(cfi);
if (!location || !location.node) {
const errorMsg = "JS: Could not find node for CFI.";
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: $ {
errorMsg
}
`);
return errorMsg;
}
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: Node found for CFI. Node type: $ {
location.node.nodeName
}
, Text content: '${(location.node.textContent || "").substring(0, 50)}...' `);
const baseNode = location.node;
let remainingOffset = startOffset;
const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
treeWalker.currentNode = baseNode;
let currentNode = baseNode.nodeType === Node.TEXT_NODE ? baseNode : treeWalker.nextNode();
// 1. Find the starting text node and character position
while (currentNode && remainingOffset >= currentNode.nodeValue.length) {
remainingOffset -= currentNode.nodeValue.length;
currentNode = treeWalker.nextNode();
}
if (!currentNode) {
const errorMsg = "JS: Text offset is out of bounds for the CFI node.";
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: $ {
errorMsg
}
`);
return errorMsg;
}
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: Start node for range found. Node: '${currentNode.nodeValue.substring(0, 50)}...', calculated offset: $ {
remainingOffset
}
`);
const range = document.createRange();
range.setStart(currentNode, remainingOffset);
// 2. Find the ending text node and character position
let remainingTextLength = textToHighlight.length;
let endNode = currentNode;
let endOffset = remainingOffset;
let sanityCheck = 0;
while (remainingTextLength > 0 && endNode && sanityCheck < 50) {
const availableLength = endNode.nodeValue.length - endOffset;
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: Finding end range. Remaining text: $ {
remainingTextLength
}
, Current node: '${endNode.nodeValue.substring(0, 50)}...', available length: $ {
availableLength
}
`);
if (availableLength >= remainingTextLength) {
endOffset += remainingTextLength;
remainingTextLength = 0;
} else {
remainingTextLength -= availableLength;
// Important: We need a fresh walker starting from the endNode to find the *next* text node reliably
const nextNodeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
nextNodeWalker.currentNode = endNode;
endNode = nextNodeWalker.nextNode();
endOffset = 0; // Start from the beginning of the next node
}
sanityCheck++;
}
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: End node for range found. End node: '${endNode ? endNode.nodeValue.substring(0, 50) : "null"}', End offset: $ {
endOffset
}
`);
if (remainingTextLength > 0) {
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: Text to highlight was longer than found text nodes. Highlighting to end of last found node.`);
if (endNode) {
range.setEnd(endNode, endNode.nodeValue.length);
} else {
const lastKnownGoodNode = range.endContainer;
range.setEnd(lastKnownGoodNode, lastKnownGoodNode.nodeValue.length);
}
} else {
range.setEnd(endNode, endOffset);
}
const highlightSpan = document.createElement("span");
highlightSpan.className = "tts-highlight";
try {
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: Attempting to surround content with highlight span.`);
range.surroundContents(highlightSpan);
} catch (e) {
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: surroundContents failed, using fallback. Error: $ {
e.message
}
`);
const contents = range.extractContents();
highlightSpan.appendChild(contents);
range.insertNode(highlightSpan);
}
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: Highlight successful. Scrolling into view.`);
highlightSpan.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
return "JS: Highlight successful.";
} catch (e) {
const errorMsg = "JS: Error during highlightFromCfi: " + e.message;
console.log(`$ {
TTS_HIGHLIGHT_LOG_TAG
}
: $ {
errorMsg
}
`);
return errorMsg;
}
};
window.extractTextWithCfiFromTop = function () {
try {
const ttsNodeSelector = "p, h1, h2, h3, h4, h5, h6, li, blockquote";
const allContentNodesRaw = Array.from(document.body.querySelectorAll(ttsNodeSelector));
// FIX: Filter out parents that contain matching children to prevent duplicates
const allContentNodes = allContentNodesRaw.filter(node => node.querySelector(ttsNodeSelector) === null);
let startBlock = null;
let startIndex = -1;
for (let i = 0; i < allContentNodes.length; i++) {
const node = allContentNodes[i];
const rect = node.getBoundingClientRect();
if (rect.bottom > (window.VIEWPORT_PADDING_TOP + 10)) {
startBlock = node;
startIndex = i;
break;
}
}
if (!startBlock) {
return window.extractTextWithCfi();
}
const nodesToProcess = allContentNodes.slice(startIndex);
const results =[];
nodesToProcess.forEach((node) => {
const text = node.innerText ? node.innerText.trim() : "";
if (text.length > 0 && node.offsetParent !== null) {
try {
const cfiObj = getCfiPathForElement(node, 0);
if (cfiObj && cfiObj.cfi) {
results.push({ cfi: cfiObj, text: text });
}
} catch (e) {}
}
});
return JSON.stringify(results);
} catch (e) {
return window.extractTextWithCfi();
}
};
window.extractTextWithCfi = function () {
const results =[];
const ttsNodeSelector = "p, h1, h2, h3, h4, h5, h6, li, blockquote";
const contentNodesRaw = Array.from(document.body.querySelectorAll(ttsNodeSelector));
const contentNodes = contentNodesRaw.filter(node => node.querySelector(ttsNodeSelector) === null);
contentNodes.forEach((node) => {
const text = node.innerText ? node.innerText.trim() : "";
if (text.length > 0 && node.offsetParent !== null) {
try {
const cfi = getCfiPathForElement(node, 0);
if (cfi) {
results.push({ cfi: cfi, text: text });
}
} catch (e) {}
}
});
return JSON.stringify(results);
};
window.extractTextWithCfiFromSelection = function() {
try {
var selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || selection.toString().trim() === "") {
return window.extractTextWithCfiFromTop(); // Fallback
}
var range = selection.getRangeAt(0);
var startNode = range.startContainer;
var startOffset = range.startOffset;
const ttsNodeSelector = "p, h1, h2, h3, h4, h5, h6, li, blockquote";
let startBlock = startNode.nodeType === Node.TEXT_NODE ? startNode.parentNode.closest(ttsNodeSelector) : startNode.closest(ttsNodeSelector);
if (!startBlock) return window.extractTextWithCfiFromTop(); // Fallback
let absoluteStartOffset = 0;
const treeWalker = document.createTreeWalker(startBlock, NodeFilter.SHOW_TEXT, null, false);
let currentNode = treeWalker.nextNode();
while (currentNode && currentNode !== startNode) {
absoluteStartOffset += currentNode.nodeValue.length;
currentNode = treeWalker.nextNode();
}
if (currentNode === startNode) {
absoluteStartOffset += startOffset;
}
const allContentNodesRaw = Array.from(document.body.querySelectorAll(ttsNodeSelector));
const allContentNodes = allContentNodesRaw.filter(node => node.querySelector(ttsNodeSelector) === null);
let startIndex = allContentNodes.findIndex(node => node === startBlock);
if (startIndex === -1) {
startIndex = allContentNodes.findIndex(node => startBlock.contains(node));
}
if (startIndex === -1) return window.extractTextWithCfiFromTop();
const nodesToProcess = allContentNodes.slice(startIndex);
const results =[];
nodesToProcess.forEach((node, index) => {
let fullText = node.textContent || "";
if (fullText.trim().length > 0 && node.offsetParent !== null) {
if (node.tagName === 'LI' || node.closest('li')) {
console.log("TTS_LIST_DIAG: Selection extracting node <" + node.tagName + "> inside LI. Text: '" + fullText.substring(0, 30) + "'");
}
try {
const cfiObj = getCfiPathForElement(node, 0);
if (cfiObj && cfiObj.cfi) {
if (index === 0) {
let sliced = fullText.substring(absoluteStartOffset);
if (sliced.trim().length > 0) {
results.push({
cfi: cfiObj,
text: sliced,
startOffset: absoluteStartOffset
});
}
} else {
results.push({
cfi: cfiObj,
text: fullText,
startOffset: 0
});
}
}
} catch(e){}
}
});
return JSON.stringify(results);
} catch(e) {
return window.extractTextWithCfiFromTop();
}
};
window.TtsBridgeHelper = {
extractAndRelayText: function () {
try {
const structuredTextJson = window.extractTextWithCfiFromTop();
if (typeof TtsBridge !== "undefined" && TtsBridge.onStructuredTextExtracted) {
TtsBridge.onStructuredTextExtracted(structuredTextJson);
}
} catch (e) {
if (typeof TtsBridge !== "undefined" && TtsBridge.onStructuredTextExtracted) {
TtsBridge.onStructuredTextExtracted("[]");
}
}
},
extractAndRelayTextFromSelection: function() {
try {
const structuredTextJson = window.extractTextWithCfiFromSelection();
if (typeof TtsBridge !== "undefined" && TtsBridge.onStructuredTextExtracted) {
TtsBridge.onStructuredTextExtracted(structuredTextJson);
}
} catch (e) {
if (typeof TtsBridge !== "undefined" && TtsBridge.onStructuredTextExtracted) {
TtsBridge.onStructuredTextExtracted("[]");
}
}
}
};
window.reportTopChunk = function () {
if (typeof ProgressReporter === "undefined" || typeof ProgressReporter.updateTopChunk === "undefined") return;
const topElement = document.elementFromPoint(window.innerWidth / 2, window.VIEWPORT_PADDING_TOP + 1);
if (topElement) {
const chunkContainer = topElement.closest("[data-chunk-index]");
if (chunkContainer) {
const chunkIndex = parseInt(chunkContainer.dataset.chunkIndex, 10);
if (!isNaN(chunkIndex)) {
ProgressReporter.updateTopChunk(chunkIndex);
}
} else {
ProgressReporter.updateTopChunk(0);
}
}
};
window.AiBridgeHelper = {
extractAndRelayTextForSummarization: function () {
var textContent = "";
try {
var mainElement = document.querySelector('article, [role="main"], main, body');
if (mainElement) {
textContent = mainElement.innerText || mainElement.textContent || "";
} else {
textContent = document.body.innerText || document.body.textContent || "";
}
if (typeof AiBridge !== "undefined" && AiBridge.onContentExtractedForSummarization) {
AiBridge.onContentExtractedForSummarization(textContent.trim());
}
} catch (e) {
if (typeof AiBridge !== "undefined" && AiBridge.onContentExtractedForSummarization) {
AiBridge.onContentExtractedForSummarization("");
}
}
},
};
window.checkImagesForDiagnosis = function () {
const images = document.querySelectorAll("img, image"); // 'image' for SVG images
const logTag = "ImageDiagnosis";
console.log(logTag + ": JS checkImagesForDiagnosis called. Found " + images.length + " image elements.");
images.forEach((img, index) => {
const src = img.src || img.getAttribute("xlink:href");
console.log(logTag + ": Image #" + index + " | src: '" + src + "'");
function processImage() {
console.log(logTag + ": Processing Image #" + index + " (complete=" + img.complete + ")");
console.log(logTag + ": Image #" + index + " | clientWidth: " + img.clientWidth + ", clientHeight: " + img.clientHeight);
console.log(logTag + ": Image #" + index + " | naturalWidth: " + img.naturalWidth + ", naturalHeight: " + img.naturalHeight);
console.log(logTag + ": Image #" + index + " | is visible (offsetParent): " + (img.offsetParent !== null));
const style = window.getComputedStyle(img);
console.log(
logTag +
": Image #" +
index +
" | computed display: '" +
style.display +
"', visibility: '" +
style.visibility +
"', opacity: '" +
style.opacity +
"'",
);
// FIX: If height has collapsed, manually calculate and set it forcefully.
if (img.complete && img.naturalWidth > 0 && img.clientWidth > 0 && img.clientHeight === 0) {
console.log(logTag + ": CORRECTING GEOMETRY for Image #" + index);
const parent = img.parentElement;
if (parent) {
const parentStyle = window.getComputedStyle(parent);
console.log(
logTag +
": Parent <" +
parent.tagName +
"> computed height: " +
parentStyle.height +
", overflow: " +
parentStyle.overflow,
);
// Force the parent's height to be determined by its content. This is crucial.
parent.style.setProperty("height", "auto", "important");
}
const aspectRatio = img.naturalHeight / img.naturalWidth;
const correctHeight = img.clientWidth * aspectRatio;
// Remove the conflicting max-height property and then set the explicit height.
img.style.setProperty("max-height", "none", "important");
img.style.setProperty("height", correctHeight + "px", "important");
console.log(logTag + ": Corrective styles applied to Image #" + index + ". Verifying height after a short delay for reflow...");
// After applying styles, wait a moment for the browser to reflow the layout
// before reporting the new height and updating the scroll state.
setTimeout(
function () {
console.log(logTag + ": Verified height for Image #" + index + ": " + img.clientHeight + "px");
window.reportScrollState(); // Update scroll metrics now that the image has height
},
150,
);
}
img.onerror = function () {
console.log(logTag + ": ERROR: Image #" + index + " FAILED to load. Src was: '" + src + "'");
};
if (img.complete && img.naturalWidth === 0) {
console.log(
logTag + ": WARNING: Image #" + index + " is complete but has 0 naturalWidth, may indicate loading error. Src: '" + src + "'",
);
}
}
if (img.complete) {
processImage();
} else {
img.onload = processImage;
}
});
};
const CFI_LOG_TAG = "CFI_DIAGNOSIS";
function log(message) {
console.log(`$ {
CFI_LOG_TAG
}
: $ {
message
}
`);
}
function resolveCfiPath(rootElement, path) {
let currentNode = rootElement;
const steps = path.substring(1).split("/").map(Number);
for (let i = 0; i < steps.length; i++) {
const cfiIndex = steps[i];
if (!currentNode) return null;
// Handle virtualized content container specially
if (currentNode.id === 'content-container') {
const childNodeIndex = (cfiIndex - 2) / 2;
let chunkIndex = Math.floor(childNodeIndex / 20);
let indexInChunk = childNodeIndex % 20;
let chunkElement = currentNode.querySelector(`.chunk-container[data-chunk-index="${chunkIndex}"]`);
if (chunkElement) {
if (chunkElement.innerHTML === "" && window.virtualization && window.virtualization.chunksData[chunkIndex]) {
console.log("CFI_DIAGNOSIS: Chunk " + chunkIndex + " was empty, restoring content for CFI resolution.");
chunkElement.innerHTML = window.virtualization.chunksData[chunkIndex];
chunkElement.style.height = "";
if (window.CURRENT_HIGHLIGHTS) {
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
}
}
let elementsInChunk = Array.from(chunkElement.childNodes).filter(n => n.nodeType === Node.ELEMENT_NODE);
if (indexInChunk >= 0 && indexInChunk < elementsInChunk.length) {
currentNode = elementsInChunk[indexInChunk];
continue;
}
}
return null;
}
let elementChildren = Array.from(currentNode.childNodes).filter((node) => node.nodeType === Node.ELEMENT_NODE);
const childNodeIndex = (cfiIndex - 2) / 2;
if (childNodeIndex >= 0 && childNodeIndex < elementChildren.length) {
currentNode = elementChildren[childNodeIndex];
} else {
return null;
}
}
return currentNode;
}
window.getNodeAndOffsetFromCfi = function (cfi) {
try {
var pathParts = cfi.split(":");
var nodePath = pathParts[0];
var charOffset = pathParts.length > 1 ? parseInt(pathParts[1], 10) : 0;
let cfiRoot = document.getElementById("content-container") || document.body;
let pathToResolve = nodePath;
// Strip the /4 prefix because cfiRoot (content-container or body) represents /4
if (pathToResolve.startsWith("/4/")) {
pathToResolve = "/" + pathToResolve.substring(3);
} else if (pathToResolve === "/4") {
pathToResolve = "";
}
if (!pathToResolve) {
return { node: cfiRoot, offset: charOffset };
}
let resolvedNode = resolveCfiPath(cfiRoot, pathToResolve);
if (!resolvedNode) return null;
let currentNode = resolvedNode;
if (currentNode.nodeType === Node.ELEMENT_NODE) {
const treeWalker = document.createTreeWalker(currentNode, NodeFilter.SHOW_TEXT, null, false);
const firstTextNode = treeWalker.nextNode();
if (firstTextNode) {
currentNode = firstTextNode;
}
}
return { node: currentNode, offset: charOffset };
} catch (e) {
return null;
}
};
window.getCfiPathForElement = function (element, charOffset) {
const logStack = [];
try {
var path =[];
var currentNode = element;
if (currentNode.nodeType === Node.TEXT_NODE) {
var accumulatedOffset = charOffset || 0;
var sibling = currentNode.previousSibling;
while (sibling) {
if (sibling.nodeType === Node.TEXT_NODE) {
accumulatedOffset += sibling.nodeValue.length;
} else if (sibling.nodeType === Node.ELEMENT_NODE) {
accumulatedOffset += (sibling.textContent || "").length;
}
sibling = sibling.previousSibling;
}
charOffset = accumulatedOffset;
currentNode = currentNode.parentNode;
}
const root = document.getElementById("content-container") || document.body;
while (currentNode && currentNode !== root && currentNode.parentNode) {
let parentNode = currentNode.parentNode;
if (parentNode.classList && parentNode.classList.contains('chunk-container')) {
let trueParent = parentNode.parentNode;
let elementSiblingsInChunk = Array.from(parentNode.childNodes).filter(node => node.nodeType === Node.ELEMENT_NODE);
let indexInChunk = elementSiblingsInChunk.indexOf(currentNode);
if (indexInChunk === -1) {
currentNode = trueParent;
continue;
}
let chunkIndex = parseInt(parentNode.dataset.chunkIndex, 10);
let elementsInPrecedingChunks = chunkIndex * 20;
let trueIndex = elementsInPrecedingChunks + indexInChunk;
let cfiIndex = trueIndex * 2 + 2;
path.unshift(cfiIndex);
logStack.push(`Chunk ${chunkIndex}, IdxInChunk ${indexInChunk}, TrueIdx ${trueIndex} -> CFI /${cfiIndex}`);
currentNode = trueParent;
continue;
}
const elementSiblings = Array.from(parentNode.childNodes).filter((node) => node.nodeType === Node.ELEMENT_NODE);
const nodeIndex = elementSiblings.indexOf(currentNode);
if (nodeIndex === -1) {
currentNode = parentNode;
continue;
}
const cfiIndex = nodeIndex * 2 + 2;
path.unshift(cfiIndex);
currentNode = parentNode;
}
var cfi = "/4";
if (path.length > 0) {
cfi += "/" + path.join("/");
}
if (charOffset !== undefined && charOffset > 0) {
cfi += ":" + charOffset;
}
logStack.push(`Generated CFI: ${cfi}`);
return { cfi: cfi, log: logStack };
} catch (e) {
logStack.push("Error: " + e.message);
return { cfi: "/4/2", log: logStack };
}
};
window.getCurrentCfi = function() {
const debugLog =[];
let finalCfi = "/4/2";
try {
const viewportX = window.innerWidth / 2;
let viewportY = window.VIEWPORT_PADDING_TOP + 5;
let topElement = null;
for (let i = 0; i < 10; i++) {
let el = document.elementFromPoint(viewportX, viewportY);
if (el && el.id !== 'content-container' &&
!(el.classList && el.classList.contains('chunk-container') && el.innerText.trim() === "")) {
topElement = el;
break;
}
viewportY += 15;
}
if (!topElement || topElement.id === 'content-container' || (topElement.classList && topElement.classList.contains('chunk-container'))) {
const elements = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, span');
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
const rect = el.getBoundingClientRect();
if (rect.bottom > window.VIEWPORT_PADDING_TOP && el.innerText.trim().length > 0) {
topElement = el;
break;
}
}
}
if (!topElement) {
topElement = document.body.firstElementChild;
}
if (!topElement) {
return JSON.stringify({ cfi: finalCfi, log: debugLog });
}
let range = null;
if (document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(viewportX, viewportY);
}
let nodeForCfi;
let offsetForCfi = 0;
if (range && range.startContainer && range.startContainer.nodeType === Node.TEXT_NODE) {
nodeForCfi = range.startContainer;
offsetForCfi = range.startOffset;
debugLog.push(`Caret range hit successfully at offset ${offsetForCfi}`);
} else {
const treeWalker = document.createTreeWalker(topElement, NodeFilter.SHOW_TEXT, null, false);
let firstTextNode = treeWalker.nextNode();
nodeForCfi = (firstTextNode && firstTextNode.textContent.trim().length > 0) ? firstTextNode : topElement;
offsetForCfi = 0;
}
const cfiResult = window.getCfiPathForElement(nodeForCfi, offsetForCfi);
finalCfi = cfiResult.cfi;
if (cfiResult.log) debugLog.push(...cfiResult.log);
} catch (e) {
debugLog.push("Error in getCurrentCfi: " + e.message);
}
return JSON.stringify({ cfi: finalCfi, log: debugLog });
};
const TAG_BM = "BookmarkDiagnosis";
function logBm(msg) {
console.log(TAG_BM + ": " + msg);
}
window.scrollToCfi = function(cfi) {
let cleanCfi = cfi;
if (cfi && cfi.includes('@')) {
cleanCfi = cfi.substring(cfi.indexOf('@') + 1);
}
console.log("NavDiag: JS scrollToCfi called with cleanCfi=" + cleanCfi);
if (!cleanCfi || !cleanCfi.startsWith('/')) {
if (window.CfiBridge && window.CfiBridge.onScrollFinished) {
window.CfiBridge.onScrollFinished(false);
}
return;
}
let attempts = 0;
const maxAttempts = 20;
let stabilizingFrames = 0;
const maxStabilizingFrames = 8;
function attemptScroll() {
attempts++;
try {
const location = window.getNodeAndOffsetFromCfi(cleanCfi);
if (location && location.node) {
if (!document.body.contains(location.node)) {
if (attempts < maxAttempts) setTimeout(attemptScroll, 100);
return;
}
let targetScrollY = 0;
if (location.node.nodeType === Node.TEXT_NODE && location.offset > 0) {
try {
console.log("PosSaveDiag: Initial text node length=" + location.node.nodeValue.length + ", location.offset=" + location.offset);
let currentNode = location.node;
let remainingOffset = location.offset;
// Traverse sibling text nodes to find the exact offset
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
walker.currentNode = currentNode;
while (currentNode && remainingOffset > currentNode.nodeValue.length) {
remainingOffset -= currentNode.nodeValue.length;
const next = walker.nextNode();
if (!next) {
console.log("PosSaveDiag: Reached end of text nodes while traversing.");
break;
}
currentNode = next;
}
console.log("PosSaveDiag: Traversed to node with text: '" + currentNode.nodeValue.substring(0, 20) + "', remainingOffset=" + remainingOffset + ", actual length=" + currentNode.nodeValue.length);
const range = document.createRange();
const validOffset = Math.min(remainingOffset, currentNode.nodeValue.length);
range.setStart(currentNode, validOffset);
range.collapse(true);
const rect = range.getBoundingClientRect();
if (rect.top !== 0 || rect.bottom !== 0) {
targetScrollY = window.scrollY + rect.top - (window.VIEWPORT_PADDING_TOP + 5);
}
} catch (e) {
console.log("PosSaveDiag: Error calculating range bounding rect: " + e.message);
}
}
if (targetScrollY === 0) {
const targetElement = (location.node.nodeType === Node.TEXT_NODE) ? location.node.parentNode : location.node;
const rect = targetElement.getBoundingClientRect();
targetScrollY = window.scrollY + rect.top - (window.VIEWPORT_PADDING_TOP + 5);
}
if (Math.abs(window.scrollY - targetScrollY) > 1) {
console.log("NavDiag: Scrolling to targetY=" + targetScrollY);
window.scrollTo({ top: targetScrollY, behavior: 'auto' });
}
stabilizingFrames++;
if (stabilizingFrames < maxStabilizingFrames) {
setTimeout(attemptScroll, 100);
} else {
setTimeout(() => {
window.reportScrollState();
if (window.CfiBridge && window.CfiBridge.onScrollFinished) {
window.CfiBridge.onScrollFinished(true);
}
}, 50);
}
} else {
if (attempts < maxAttempts) {
setTimeout(attemptScroll, 100);
} else {
if (window.CfiBridge && window.CfiBridge.onScrollFinished) {
window.CfiBridge.onScrollFinished(false);
}
}
}
} catch (e) {
if (window.CfiBridge && window.CfiBridge.onScrollFinished) {
window.CfiBridge.onScrollFinished(false);
}
}
}
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(function() {
attemptScroll();
});
} else {
attemptScroll();
}
};
window.getElementByCfi = function(cfi) {
logBm("getElementByCfi called with: " + cfi);
try {
const location = window.getNodeAndOffsetFromCfi(cfi);
if (location && location.node) {
logBm("Found node: " + location.node.nodeName);
return (location.node.nodeType === Node.TEXT_NODE) ? location.node.parentNode : location.node;
}
logBm("Node not found.");
return null;
} catch (e) {
logBm("Error: " + e.message);
return null;
}
};
window.isElementInViewport = function (el) {
if (!el || typeof el.getBoundingClientRect !== "function") return false;
const rect = el.getBoundingClientRect();
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
return rect.top >= 0 && rect.top <= viewportHeight;
};
window.getSnippetForCfi = function (cfi) {
var TAG_DIAG = "BookmarkDiagnosis";
console.log(TAG_DIAG + ": getSnippetForCfi called with CFI: " + cfi);
const location = window.getNodeAndOffsetFromCfi(cfi);
if (location && location.node) {
let textNode = location.node.nodeType === Node.TEXT_NODE ? location.node : null;
let offset = location.offset;
if (!textNode) {
const treeWalker = document.createTreeWalker(location.node, NodeFilter.SHOW_TEXT, null, false);
textNode = treeWalker.nextNode();
offset = 0;
} else if (offset > 0) {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
walker.currentNode = textNode;
while (textNode && offset > textNode.nodeValue.length) {
offset -= textNode.nodeValue.length;
const next = walker.nextNode();
if (!next) break;
textNode = next;
}
}
if (textNode) {
const fullText = textNode.textContent;
const lastSpace = fullText.lastIndexOf(" ", offset);
const startIndex = lastSpace === -1 ? 0 : lastSpace + 1;
let snippet = fullText.substring(startIndex);
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
walker.currentNode = textNode;
while (snippet.length < 160) {
const nextNode = walker.nextNode();
if (nextNode) {
snippet += " " + nextNode.textContent;
} else {
break;
}
}
const finalSnippet = snippet.trim().replace(/\s+/g, " ").substring(0, 150);
console.log(TAG_DIAG + ": Snippet generated: '" + finalSnippet + "'");
return finalSnippet;
}
}
console.log(TAG_DIAG + ": No usable text found for CFI. Returning empty snippet.");
return "";
};
window.findFirstVisibleCfi = function (cfiArray) {
if (!Array.isArray(cfiArray)) {
return null;
}
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const activationZoneEnd = window.VIEWPORT_PADDING_TOP + (viewportHeight - window.VIEWPORT_PADDING_TOP) * 0.6;
for (const cfi of cfiArray) {
const location = window.getNodeAndOffsetFromCfi(cfi);
if (location && location.node) {
try {
const node = location.node;
// Use the element's rect for stability, not a potentially zero-height range rect.
const elementForRect = node.nodeType === Node.TEXT_NODE ? node.parentNode : node;
const rect = elementForRect.getBoundingClientRect();
const nodeName = elementForRect.nodeName;
// The element is "active" if:
// 1. It's positioned at the top of the content, even if under the padding.
const isAtContentTop = rect.top >= 0 && rect.top < window.VIEWPORT_PADDING_TOP;
// 2. Any part of it overlaps with the main "reading zone" below the padding.
const isInReadingZone = rect.bottom > window.VIEWPORT_PADDING_TOP && rect.top < activationZoneEnd;
if ((isAtContentTop && rect.height > 0) || isInReadingZone) {
return cfi;
}
} catch (e) {
console.log(TAG + ": Error processing CFI " + cfi + ": " + e.message);
}
} else {
console.log(TAG + ": No node found for CFI: " + cfi);
}
}
console.log(TAG + ": No visible bookmarked element found in viewport. Returning null.");
return null;
};
})();
(function () {
// --- VIRTUALIZATION LOGIC ---
if (window.virtualization) return;
let currentBottomChunkIndex = 0;
let totalChunks = 0;
let isLoading = false;
let observer;
window.virtualization = {
totalChunks: 0,
chunksData: [],
chunkHeights: [],
observer: null,
init: function (initialChunkIndex, total) {
console.log(`Virtualization: Init with ${total} chunks. Anchor: ${initialChunkIndex}`);
this.totalChunks = total;
this.chunksData = new Array(total).fill(null);
this.chunkHeights = new Array(total).fill(0);
const container = document.getElementById("content-container");
if (container) {
container.querySelectorAll(".chunk-container").forEach((div) => {
let idx = parseInt(div.dataset.chunkIndex, 10);
let content = div.innerHTML;
if (content.trim().length > 0) {
this.chunksData[idx] = content;
this.chunkHeights[idx] = div.getBoundingClientRect().height;
}
});
}
this.setupObserver();
},
setupObserver: function () {
if (this.observer) this.observer.disconnect();
this.observer = new IntersectionObserver(
(entries) => {
let scrollAdjust = 0;
let domChanged = false;
entries.forEach((entry) => {
let div = entry.target;
let idx = parseInt(div.dataset.chunkIndex, 10);
if (entry.isIntersecting) {
if (!this.chunksData[idx]) {
if (window.ContentBridge && window.ContentBridge.requestChunk) {
window.ContentBridge.requestChunk(idx);
}
} else if (div.innerHTML === "") {
let oldHeight = div.getBoundingClientRect().height;
div.innerHTML = this.chunksData[idx];
div.style.height = "";
let newHeight = div.getBoundingClientRect().height;
this.chunkHeights[idx] = newHeight;
domChanged = true;
if (div.getBoundingClientRect().top < 0) {
scrollAdjust += (newHeight - oldHeight);
}
if (window.CURRENT_HIGHLIGHTS) {
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
}
if (window.CURRENT_SEARCH_QUERY) {
window.highlightAllOccurrences(window.CURRENT_SEARCH_QUERY);
}
}
} else {
if (div.innerHTML !== "") {
let oldHeight = div.getBoundingClientRect().height;
this.chunkHeights[idx] = oldHeight;
div.style.height = oldHeight + "px";
div.innerHTML = "";
domChanged = true;
}
}
});
if (scrollAdjust !== 0) {
window.scrollBy(0, scrollAdjust);
}
if (domChanged && window.reportScrollState) {
setTimeout(window.reportScrollState, 50);
}
},
{ rootMargin: "2500px 0px" }
);
document.querySelectorAll(".chunk-container").forEach((div) => {
this.observer.observe(div);
});
},
appendChunk: function (index, htmlContent) {
console.log(`Virtualization: Receiving chunk ${index} from Kotlin`);
if (Array.isArray(this.chunksData)) {
this.chunksData[index] = htmlContent;
}
let div = document.querySelector(`.chunk-container[data-chunk-index="${index}"]`);
if (div && div.innerHTML === "") {
let oldHeight = div.getBoundingClientRect().height;
div.innerHTML = htmlContent;
div.style.height = "";
let newHeight = div.getBoundingClientRect().height;
if (Array.isArray(this.chunkHeights)) {
this.chunkHeights[index] = newHeight;
}
if (div.getBoundingClientRect().bottom < 0) {
window.scrollBy(0, newHeight - oldHeight);
}
if (window.reportScrollState) {
setTimeout(window.reportScrollState, 50);
}
if (window.CURRENT_HIGHLIGHTS) {
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
}
if (window.CURRENT_SEARCH_QUERY) {
window.highlightAllOccurrences(window.CURRENT_SEARCH_QUERY);
}
}
if (window.checkImagesForDiagnosis) {
setTimeout(window.checkImagesForDiagnosis, 100);
}
},
};
const HL_LOG_TAG = "HIGHLIGHT_DEBUG";
window.HighlightBridgeHelper = {
updateHighlightStyle: function (cfi, newColorClass, colorId) {
console.log(`${HL_LOG_TAG}: updateHighlightStyle called. CFI: ${cfi}, Class: ${newColorClass}`);
var allSpans = document.querySelectorAll('span[class*="user-highlight-"]');
allSpans.forEach((span) => {
var currentCfiAttr = span.getAttribute("data-cfi") || "";
var cfis = currentCfiAttr.split(";;");
if (cfis.includes(cfi)) {
var classesToRemove = [];
for (var i = 0; i < span.classList.length; i++) {
if (span.classList[i].startsWith("user-highlight-")) {
classesToRemove.push(span.classList[i]);
}
}
classesToRemove.forEach((cls) => span.classList.remove(cls));
span.classList.add(newColorClass);
}
});
if (window.HighlightBridge) {
var sampleSpan = document.querySelector(`span[data-cfi*='${cfi}']`);
var text = sampleSpan ? sampleSpan.textContent : "";
window.HighlightBridge.onHighlightCreated(cfi, text, colorId);
}
},
createUserHighlight: function (colorClass, colorId) {
console.log(`$ {
HL_LOG_TAG
}
: createUserHighlight. Class: $ {
colorClass
}
`);
var selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || selection.toString().trim() === "") {
return;
}
try {
var range = selection.getRangeAt(0);
var text = range.toString();
var safeStartNode = range.startContainer;
var safeStartOffset = range.startOffset;
var safeEndNode = range.endContainer;
var safeEndOffset = range.endOffset;
console.log(`$ {
HL_LOG_TAG
}
: [CREATE] Raw Selection - Text: '${text}' `);
console.log(`$ {
HL_LOG_TAG
}
: [CREATE] StartContainer Type: $ {
safeStartNode.nodeType
}
, Name: $ {
safeStartNode.nodeName
}
`);
var startResult = getCfiPathForElement(safeStartNode, safeStartOffset);
var startCfi = startResult.cfi;
var endResult = getCfiPathForElement(safeEndNode, safeEndOffset);
var endCfi = endResult.cfi;
var finalCfi = startCfi;
if (startCfi !== endCfi) {
finalCfi = startCfi + "|" + endCfi;
console.log(`$ {
HL_LOG_TAG
}
: [CREATE] Range CFI Detected. Start: $ {
startCfi
}
, End: $ {
endCfi
}
`);
}
console.log(`$ {
HL_LOG_TAG
}
: [CREATE] Final CFI: $ {
finalCfi
}
`);
range = this.normalizeRangeBoundaries(range);
this.highlightRangeSafe(range, colorClass, finalCfi);
selection.removeAllRanges();
if (window.HighlightBridge) {
window.HighlightBridge.onHighlightCreated(finalCfi, text, colorId);
}
} catch (e) {
console.log(
`$ {
HL_LOG_TAG
}
: Create Error: ` + e.message,
);
}
},
normalizeRangeBoundaries: function (range) {
var startContainer = range.startContainer;
var startOffset = range.startOffset;
var endContainer = range.endContainer;
var endOffset = range.endOffset;
if (startContainer.nodeType === Node.TEXT_NODE && startOffset > 0 && startOffset < startContainer.length) {
var newStartNode = startContainer.splitText(startOffset);
range.setStart(newStartNode, 0);
if (endContainer === startContainer) {
endContainer = newStartNode;
endOffset = endOffset - startOffset;
}
}
if (endContainer.nodeType === Node.TEXT_NODE && endOffset > 0 && endOffset < endContainer.length) {
endContainer.splitText(endOffset);
range.setEnd(endContainer, endOffset);
}
return range;
},
highlightRangeSafe: function (range, className, newCfi) {
var nodes = this.getTextNodesInRange(range);
nodes.forEach((node) => {
var parent = node.parentNode;
if (parent && parent.tagName === "SPAN" && parent.classList.contains(className)) {
var currentCfi = parent.getAttribute("data-cfi") || "";
var cfiList = currentCfi.split(";;");
if (!cfiList.includes(newCfi)) {
parent.setAttribute("data-cfi", currentCfi ? (currentCfi + ";;" + newCfi) : newCfi);
}
} else {
if (node.nodeValue.trim().length === 0) return;
var span = document.createElement("span");
span.className = className;
span.setAttribute("data-cfi", newCfi);
node.parentNode.insertBefore(span, node);
span.appendChild(node);
}
});
},
getTextNodesInRange: function (range) {
var textNodes = [];
var container = range.commonAncestorContainer;
var root = container.nodeType === Node.TEXT_NODE ? container.parentNode : container;
var walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (node) {
return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
},
},
false,
);
while (walker.nextNode()) {
textNodes.push(walker.currentNode);
}
return textNodes;
},
removeHighlightByCfi: function (cfiToRemove, optionalCssClass) {
console.log(`$ {
HL_LOG_TAG
}
: removeHighlightByCfi called.`);
console.log(`$ {
HL_LOG_TAG
}
: -> Target CFI: '${cfiToRemove}' `);
console.log(`$ {
HL_LOG_TAG
}
: -> Optional Class: '${optionalCssClass}' `);
// 1. Select all highlight spans manually to avoid selector syntax errors
var allSpans = document.querySelectorAll("span[data-cfi]");
console.log(`$ {
HL_LOG_TAG
}
: Total highlight spans found in DOM: $ {
allSpans.length
}
`);
var foundCount = 0;
var removedCount = 0;
var updatedCount = 0;
allSpans.forEach((span) => {
var currentCfiAttr = span.getAttribute("data-cfi") || "";
var cfiList = currentCfiAttr.split(";;");
// Detailed check for match
if (cfiList.includes(cfiToRemove)) {
foundCount++;
console.log(`$ {
HL_LOG_TAG
}
: Match found on span. Current CFIs: [$ {
currentCfiAttr
}
]`);
var newCfiList = cfiList.filter((c) => c !== cfiToRemove);
if (newCfiList.length === 0) {
// CASE 1: No other highlights on this span -> Remove entirely
console.log(`$ {
HL_LOG_TAG
}
: -> Removing span entirely (no remaining CFIs).`);
var parent = span.parentNode;
while (span.firstChild) parent.insertBefore(span.firstChild, span);
parent.removeChild(span);
parent.normalize();
removedCount++;
} else {
console.log(`$ {
HL_LOG_TAG
}
: -> Updating span (remaining CFIs: $ {
newCfiList.join(';;')
}).`);
span.setAttribute("data-cfi", newCfiList.join(";;"));
if (optionalCssClass) {
console.log(`$ {
HL_LOG_TAG
}
: -> Removing CSS class: $ {
optionalCssClass
}
`);
span.classList.remove(optionalCssClass);
} else {
console.log(`$ {
HL_LOG_TAG
}
: -> Warning: No CSS class provided to remove. Visual style might persist if classes are mixed.`);
}
updatedCount++;
}
}
});
console.log(`$ {
HL_LOG_TAG
}
: Removal Summary -> Matched: $ {
foundCount
}
, Removed: $ {
removedCount
}
, Updated: $ {
updatedCount
}
`);
if (foundCount === 0) {
console.log(`$ {
HL_LOG_TAG
}
: No exact matches found. Attempting legacy fallback.`);
this.removeHighlightByCfiLegacy(cfiToRemove);
}
},
removeHighlightByCfiLegacy: function (cfi) {
try {
var location = window.getNodeAndOffsetFromCfi(cfi);
if (location && location.node) {
var target = location.node;
if (target.nodeType === Node.TEXT_NODE) target = target.parentNode;
if (target.tagName === "SPAN" && target.className.startsWith("user-highlight-")) {
var parent = target.parentNode;
while (target.firstChild) parent.insertBefore(target.firstChild, target);
parent.removeChild(target);
parent.normalize();
}
}
} catch (e) {}
},
restoreHighlights: function (jsonArrayString) {
try {
var highlights = JSON.parse(jsonArrayString);
var self = this;
highlights.forEach(function (h) {
self.applyHighlight(h.cfi, h.text, h.cssClass);
});
} catch (e) {
console.log(
`$ {
HL_LOG_TAG
}
: Error restoring: ` + e.message,
);
}
},
applyHighlight: function (cfi, text, cssClass) {
try {
var alreadyApplied = false;
var spans = document.querySelectorAll(`span[data-cfi]`);
for (var i = 0; i < spans.length; i++) {
if ((spans[i].getAttribute("data-cfi") || "").split(";;").includes(cfi)) {
alreadyApplied = true;
break;
}
}
if (alreadyApplied) return;
const location = window.getNodeAndOffsetFromCfi(cfi);
if (!location || !location.node) return;
let startNode = location.node;
let startOffset = location.offset;
if (startNode.nodeType === Node.TEXT_NODE) {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
walker.currentNode = startNode;
while (startNode && startOffset >= startNode.nodeValue.length) {
if (startOffset === startNode.nodeValue.length) {
const next = walker.nextNode();
if (next) {
startOffset -= startNode.nodeValue.length;
startNode = next;
} else {
break;
}
} else {
startOffset -= startNode.nodeValue.length;
startNode = walker.nextNode();
}
}
}
// 1. Text Verification / Healing
if (text && text.length > 0 && startNode && startNode.nodeType === Node.TEXT_NODE) {
const nodeVal = startNode.nodeValue;
// Check if text matches at exact offset
const substring = nodeVal.substring(startOffset, startOffset + text.length);
// Allow for some whitespace looseness (trim comparison)
if (substring !== text && substring.trim() !== text.trim()) {
console.log(`$ {
HL_LOG_TAG
}
: Text mismatch at CFI. Searching nearby... Expected: '${text.substring(0, 10)}...', Found: '${substring.substring(0, 10)}...' `);
// Try finding the text in the whole node
const foundIndex = nodeVal.indexOf(text);
if (foundIndex !== -1) {
console.log(`$ {
HL_LOG_TAG
}
: Found text elsewhere in node. Adjusting offset from $ {
startOffset
}
to $ {
foundIndex
}
.`);
startOffset = foundIndex;
} else {
// Simple fuzzy: Try finding first 20 chars
const partial = text.substring(0, Math.min(text.length, 20));
const partialIndex = nodeVal.indexOf(partial);
if (partialIndex !== -1) {
console.log(`$ {
HL_LOG_TAG
}
: Found partial match. Adjusting offset.`);
startOffset = partialIndex;
}
}
}
}
if (!startNode) return;
const range = document.createRange();
// Set Start
if (startNode.nodeType === Node.TEXT_NODE) {
// Ensure offset is valid
if (startOffset > startNode.nodeValue.length) {
startOffset = Math.max(0, startNode.nodeValue.length - 1);
}
}
range.setStart(startNode, startOffset);
const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
treeWalker.currentNode = startNode;
let currentNode = treeWalker.currentNode;
let remainingOffset = startOffset;
let remainingLen = text.length;
let endNode = currentNode;
let endOffset = startOffset;
while (remainingLen > 0 && endNode) {
let avail = endNode.nodeValue.length - endOffset;
if (avail >= remainingLen) {
endOffset += remainingLen;
remainingLen = 0;
} else {
remainingLen -= avail;
endNode = treeWalker.nextNode();
endOffset = 0;
}
}
if (endNode) {
range.setEnd(endNode, endOffset);
var normalizedRange = this.normalizeRangeBoundaries(range);
this.highlightRangeSafe(normalizedRange, cssClass, cfi);
}
} catch (e) {
console.log(e);
}
},
};
})();
(function () {
const TAG_AUTO_SCROLL = "AutoScrollDiagnosis";
window.autoScroll = {
active: false,
speed: 1.0,
accumulator: 0.0,
animationId: null,
start: function (speed) {
console.log(`$ {
TAG_AUTO_SCROLL
}
: Starting auto-scroll. Speed: $ {
speed
}
`);
this.active = true;
this.speed = speed || this.speed;
this.accumulator = 0.0;
if (this.animationId) cancelAnimationFrame(this.animationId);
this.loop();
},
stop: function () {
this.active = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
const container = document.getElementById("content-container") || document.body;
if (container) {
container.style.transform = "none";
window.scrollBy(0, 0);
}
},
updateSpeed: function (newSpeed) {
console.log(`$ {
TAG_AUTO_SCROLL
}
: Speed updated to $ {
newSpeed
}
`);
this.speed = newSpeed;
},
loop: function () {
if (!this.active) return;
this.accumulator += this.speed;
const totalPixelsToScroll = Math.floor(this.accumulator);
if (totalPixelsToScroll >= 1) {
const prevScrollY = window.scrollY;
window.scrollBy(0, totalPixelsToScroll);
this.accumulator -= totalPixelsToScroll;
const scrollY = window.scrollY;
const docHeight = document.documentElement.scrollHeight;
const innerH = window.innerHeight;
const isAtBottom = scrollY + innerH >= docHeight - 3;
const isStuck = totalPixelsToScroll > 0 && scrollY === prevScrollY && prevScrollY > 0;
if (isAtBottom || isStuck) {
this.stop();
if (window.AutoScrollBridge && window.AutoScrollBridge.onChapterEnd) {
window.AutoScrollBridge.onChapterEnd();
}
return;
}
}
const container = document.getElementById("content-container") || document.body;
if (container) {
container.style.transform = `translate3d(0, -${this.accumulator}px, 0)`;
}
this.animationId = requestAnimationFrame(this.loop.bind(this));
},
};
})();