// epub_reader.js (function () { var READER_VERTICAL_JITTER_TAG = "EpubVerticalJitter"; function logVerticalJitter(message) { console.log(READER_VERTICAL_JITTER_TAG + ": " + message); } window.logReaderVerticalJitter = window.logReaderVerticalJitter || logVerticalJitter; 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: auto; height: auto; display: block; margin-left: auto; margin-right: auto; background-color: transparent; object-fit: contain; } #content-container a[href], #content-container a[href]:link, #content-container a[href]:visited, body a[href], body a[href]:link, body a[href]:visited, a[href], a[href]:link, a[href]:visited { color: var(--reader-link, #005FCC) !important; cursor: pointer; text-decoration-line: underline !important; text-decoration-color: var(--reader-link-decoration, var(--reader-link, #005FCC)) !important; text-decoration-thickness: 0.08em; text-decoration-thickness: max(1px, 0.08em); text-underline-offset: 0.14em; text-decoration-skip-ink: auto; background-image: linear-gradient(transparent 62%, var(--reader-link-bg, rgba(0, 95, 204, 0.16)) 62%); border-radius: 2px; } #content-container a[href] *, body a[href] *, a[href] * { color: var(--reader-link, #005FCC) !important; text-decoration-color: var(--reader-link-decoration, var(--reader-link, #005FCC)) !important; } 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); installReaderSelectionBridge(); } function installReaderSelectionBridge() { if (window.__readerSelectionBridgeInstalled) { return; } window.__readerSelectionBridgeInstalled = true; var pointerActive = false; var lastPayloadJson = ""; function buildSelectionPayload() { var selection = window.getSelection && window.getSelection(); if (!selection || selection.rangeCount === 0) { return null; } var selectedText = selection.toString().trim(); if (!selectedText) { return null; } var range = selection.getRangeAt(0); var viewportLeft = 0; var viewportTop = 0; var viewportRight = window.innerWidth || document.documentElement.clientWidth || 0; var viewportBottom = window.innerHeight || document.documentElement.clientHeight || 0; var rects = Array.prototype.slice.call(range.getClientRects ? range.getClientRects() : []); rects = rects.filter(function (rect) { if (!rect || rect.width <= 0 || rect.height <= 0) return false; return rect.right >= viewportLeft && rect.left <= viewportRight && rect.bottom >= viewportTop && rect.top <= viewportBottom; }); var rect = null; if (rects.length > 0) { var firstRect = rects[0]; rect = rects.reduce(function (acc, item) { return { left: Math.min(acc.left, item.left), top: Math.min(acc.top, item.top), right: Math.max(acc.right, item.right), bottom: Math.max(acc.bottom, item.bottom) }; }, { left: firstRect.left, top: firstRect.top, right: firstRect.right, bottom: firstRect.bottom }); rect.width = rect.right - rect.left; rect.height = rect.bottom - rect.top; } else { rect = range.getBoundingClientRect ? range.getBoundingClientRect() : null; if (!rect || rect.width <= 0 || rect.height <= 0) { return null; } } return { text: selectedText, left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height }; } function reportSelectionToBridge() { if (pointerActive) { return; } if (!window.ReaderSelectionBridge || !window.ReaderSelectionBridge.onSelectionChanged) { return; } var payload = buildSelectionPayload(); if (!payload) { lastPayloadJson = ""; return; } var payloadJson = JSON.stringify(payload); if (payloadJson === lastPayloadJson) { return; } lastPayloadJson = payloadJson; window.ReaderSelectionBridge.onSelectionChanged(payloadJson); } function scheduleSelectionReport(delay) { setTimeout(reportSelectionToBridge, delay == null ? 160 : delay); } function markPointerActive() { pointerActive = true; } function markPointerReleased() { pointerActive = false; scheduleSelectionReport(80); scheduleSelectionReport(220); scheduleSelectionReport(520); } document.addEventListener("selectionchange", function () { if (!pointerActive) { scheduleSelectionReport(160); } }); document.addEventListener("pointerdown", markPointerActive, true); document.addEventListener("pointerup", markPointerReleased, true); document.addEventListener("pointercancel", markPointerReleased, true); document.addEventListener("touchstart", markPointerActive, true); document.addEventListener("touchend", markPointerReleased, true); document.addEventListener("touchcancel", markPointerReleased, true); document.addEventListener("mousedown", markPointerActive, true); document.addEventListener("mouseup", markPointerReleased, true); document.addEventListener("keyup", function () { scheduleSelectionReport(80); }); window.addEventListener("scroll", function () { scheduleSelectionReport(80); }, false); } 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, textureAlpha) { 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 linkPalette = getReaderLinkPalette(isDark, effectiveBg, effectiveText); var effectiveTextureAlpha = Math.max(0, Math.min(1, textureAlpha == null ? 0.55 : textureAlpha)); var bgMatch = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(effectiveBg); var bgRgb = bgMatch ? `${parseInt(bgMatch[1], 16)}, ${parseInt(bgMatch[2], 16)}, ${parseInt(bgMatch[3], 16)}` : (isDark ? '18, 18, 18' : '255, 255, 255'); var textureCss = textureBase64 ? `background-image: linear-gradient(rgba(${bgRgb},${1 - effectiveTextureAlpha}), rgba(${bgRgb},${1 - effectiveTextureAlpha})), url('${textureBase64}'); background-repeat: repeat, repeat; background-blend-mode: normal, normal;` : 'background-image: none;'; var css = ` :root { --reader-bg: ${effectiveBg}; --reader-text: ${effectiveText}; --reader-link: ${linkPalette.color}; --reader-link-decoration: ${linkPalette.color}; --reader-link-bg: ${linkPalette.background}; } html.${themeClassName}, html.${themeClassName} body { background-color: var(--reader-bg) !important; color: var(--reader-text) !important; ${textureCss} } html.${themeClassName} body a[href], html.${themeClassName} body a[href]:link, html.${themeClassName} body a[href]:visited, html.${themeClassName} a[href], html.${themeClassName} a[href]:link, html.${themeClassName} a[href]:visited { color: var(--reader-link) !important; cursor: pointer; text-decoration-line: underline !important; text-decoration-color: var(--reader-link-decoration) !important; text-decoration-thickness: 0.08em; text-decoration-thickness: max(1px, 0.08em); text-underline-offset: 0.14em; text-decoration-skip-ink: auto; background-image: linear-gradient(transparent 62%, var(--reader-link-bg) 62%); border-radius: 2px; } html.${themeClassName} body a[href] p, html.${themeClassName} body a[href] div, html.${themeClassName} body a[href] span, html.${themeClassName} body a[href] li, html.${themeClassName} body a[href] h1, html.${themeClassName} body a[href] h2, html.${themeClassName} body a[href] h3, html.${themeClassName} body a[href] h4, html.${themeClassName} body a[href] h5, html.${themeClassName} body a[href] h6, html.${themeClassName} a[href] p, html.${themeClassName} a[href] div, html.${themeClassName} a[href] span, html.${themeClassName} a[href] li, html.${themeClassName} a[href] h1, html.${themeClassName} a[href] h2, html.${themeClassName} a[href] h3, html.${themeClassName} a[href] h4, html.${themeClassName} a[href] h5, html.${themeClassName} a[href] h6 { color: var(--reader-link) !important; text-decoration-color: var(--reader-link-decoration) !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 rgbToHex(rgb) { function channel(value) { var hex = Math.max(0, Math.min(255, value)).toString(16); return hex.length < 2 ? '0' + hex : hex; } return '#' + channel(rgb.r) + channel(rgb.g) + channel(rgb.b); } function contrastRatio(first, second) { var firstLum = getLuminance(first.r, first.g, first.b); var secondLum = getLuminance(second.r, second.g, second.b); var lighter = Math.max(firstLum, secondLum); var darker = Math.min(firstLum, secondLum); return (lighter + 0.05) / (darker + 0.05); } function getReaderLinkPalette(isDark, bgHex, textHex) { var bg = hexToRgb(bgHex); var text = hexToRgb(textHex); var bgLum = getLuminance(bg.r, bg.g, bg.b); var textLum = getLuminance(text.r, text.g, text.b); var candidates = (isDark || bgLum < 0.45) ? [ { r: 125, g: 211, b: 252 }, { r: 94, g: 234, b: 212 }, { r: 165, g: 180, b: 252 }, { r: 253, g: 230, b: 138 }, { r: 255, g: 255, b: 255 } ] : [ { r: 0, g: 95, b: 204 }, { r: 0, g: 109, b: 117 }, { r: 122, g: 30, b: 82 }, { r: 74, g: 20, b: 140 }, { r: 17, g: 24, b: 39 } ]; var best = candidates[0]; var bestScore = -1; for (var i = 0; i < candidates.length; i++) { var candidate = candidates[i]; var contrast = contrastRatio(candidate, bg); var separation = Math.abs(getLuminance(candidate.r, candidate.g, candidate.b) - textLum); if (contrast >= 4.5 && separation >= 0.08) { best = candidate; break; } var score = contrast * 10 + separation; if (score > bestScore) { bestScore = score; best = candidate; } } var alpha = bgLum < 0.45 ? 0.24 : 0.16; return { color: rgbToHex(best), background: `rgba(${best.r}, ${best.g}, ${best.b}, ${alpha})` }; } 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) { if (el.closest && el.closest('a[href]')) return; 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; } function getFootnoteContent(targetId) { console.log("FootnoteDiag: Searching for footnote content with id: '" + targetId + "'"); var el = document.getElementById(targetId); if (el) { console.log("FootnoteDiag: Found element directly in DOM."); return el.innerHTML; } console.log("FootnoteDiag: Element not in DOM, checking virtualized chunks."); if (window.virtualization && window.virtualization.chunksData) { for (var i = 0; i < window.virtualization.chunksData.length; i++) { var chunkHtml = window.virtualization.chunksData[i]; if (chunkHtml && chunkHtml.indexOf('id="' + targetId + '"') !== -1) { console.log("FootnoteDiag: Found potential id match in chunk " + i); var tempDiv = document.createElement('div'); tempDiv.innerHTML = chunkHtml; var found = tempDiv.querySelector('#' + targetId); if (found) { console.log("FootnoteDiag: Successfully extracted note from chunk " + i + "."); return found.innerHTML; } } } } console.log("FootnoteDiag: Footnote content not found anywhere."); return null; } // 1. Handle Taps (Click) document.addEventListener("click", function (e) { if (handleHighlightInteraction(e)) return; var target = e.target; var anchor = target.closest('a'); if (anchor) { var href = anchor.getAttribute('href'); var epubType = anchor.getAttribute('epub:type'); var linkText = (anchor.textContent || '').trim().substring(0, 80); console.log("LINK_NAV: [JS-CLICK] href='" + href + "', epub:type='" + epubType + "', label='" + linkText + "'"); if (window.LinkNavBridge && window.LinkNavBridge.onLinkClicked) { window.LinkNavBridge.onLinkClicked(href || '', epubType || '', linkText); } console.log("FootnoteDiag: Link clicked. href: '" + href + "', epub:type: '" + epubType + "'"); if ((href && href.startsWith('#')) || epubType === 'noteref') { console.log("LINK_NAV: [JS-CLASSIFY] type=FRAGMENT_OR_FOOTNOTE, href='" + href + "'"); var targetId = href ? href.substring(1) : null; console.log("FootnoteDiag: Extracted targetId: '" + targetId + "'"); if (targetId) { var content = getFootnoteContent(targetId); if (content && window.FootnoteBridge) { console.log("FootnoteDiag: Content extracted, sending to Kotlin Bridge."); e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); window.FootnoteBridge.onFootnoteRequested(content); return; } else if (!content) { console.log("FootnoteDiag: Failed to get content. Link might just be a regular anchor."); } else if (!window.FootnoteBridge) { console.log("FootnoteDiag: window.FootnoteBridge is undefined!"); } } } } else { console.log("LINK_NAV: [JS-NO-ANCHOR] No tag found in click target hierarchy"); } }, true); function getReaderImageElements() { return Array.prototype.slice.call(document.querySelectorAll("img, svg, video, canvas, image")); } function rememberReaderImageAnchors() { getReaderImageElements().forEach(function (image) { if (image.getAttribute("data-reader-image-anchor")) return; var parent = image.parentElement || document.body; var imageRect = image.getBoundingClientRect(); var parentRect = parent.getBoundingClientRect(); var parentWidth = parentRect.width || document.documentElement.clientWidth || window.innerWidth || 0; if (!parentWidth || imageRect.width <= 0) { image.setAttribute("data-reader-image-anchor", "center"); return; } var imageCenter = imageRect.left + imageRect.width / 2; var parentCenter = parentRect.left + parentWidth / 2; var tolerance = Math.max(4, parentWidth * 0.08); var anchor = "center"; if (Math.abs(imageCenter - parentCenter) <= tolerance || imageRect.width >= parentWidth - 2) { anchor = "center"; } else if (imageCenter > parentCenter) { anchor = "right"; } else { anchor = "left"; } image.setAttribute("data-reader-image-anchor", anchor); }); } function applyReaderImageAnchors() { getReaderImageElements().forEach(function (image) { var anchor = image.getAttribute("data-reader-image-anchor") || "center"; image.style.setProperty("display", "block", "important"); image.style.setProperty("height", "auto", "important"); image.style.setProperty("object-fit", "contain", "important"); if (anchor === "right") { image.style.setProperty("float", "none", "important"); image.style.setProperty("margin-left", "auto", "important"); image.style.setProperty("margin-right", "0", "important"); } else if (anchor === "left") { image.style.setProperty("margin-left", "0", "important"); image.style.setProperty("margin-right", "auto", "important"); } else { image.style.setProperty("float", "none", "important"); image.style.setProperty("margin-left", "auto", "important"); image.style.setProperty("margin-right", "auto", "important"); } }); } window.updateReaderStyles = function (fontSizeEm, lineHeight, fontFamily, textAlign, paragraphGap, imageSize, horizontalMargin, verticalMargin) { var logTag = "ReaderFontDiagnosis"; console.log( logTag + ": updateReaderStyles called. Size: " + fontSizeEm + ", LineHeight: " + lineHeight + ", Font: '" + fontFamily + "', Align: '" + textAlign + "', Gap: " + paragraphGap + ", ImageSize: " + imageSize + ", HorizontalMargin: " + horizontalMargin + ", VerticalMargin: " + verticalMargin ); 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); var newImageSize = parseFloat(imageSize); var newHorizontalMargin = parseFloat(horizontalMargin); var newVerticalMargin = parseFloat(verticalMargin); if (isNaN(newFontSize) || newFontSize < 0.5 || newFontSize > 5.0) newFontSize = 1.0; if (isNaN(newLineHeight) || newLineHeight < 1.0 || newLineHeight > 3.0) newLineHeight = 1.0; if (isNaN(newGap) || newGap < 0.0 || newGap > 3.0) newGap = 1.0; if (isNaN(newImageSize) || newImageSize < 0.5 || newImageSize > 2.0) newImageSize = 1.0; if (isNaN(newHorizontalMargin) || newHorizontalMargin < 0.0 || newHorizontalMargin > 3.0) newHorizontalMargin = 1.0; if (isNaN(newVerticalMargin) || newVerticalMargin < 0.0 || newVerticalMargin > 3.0) newVerticalMargin = 1.0; var styleSignature = [ newFontSize, newLineHeight, fontFamily || "", textAlign || "", newGap, newImageSize, newHorizontalMargin, newVerticalMargin, ].join("|"); if (window.__readerStyleSignature === styleSignature && dynamicStyleElement.innerHTML.trim().length > 0) { logVerticalJitter( "jsStyleSkip unchanged scrollY=" + Math.round(window.scrollY || window.pageYOffset || 0) + " signature=" + styleSignature, ); return false; } var scrollYBeforeStyle = Math.round(window.scrollY || window.pageYOffset || 0); var scrollHeightBeforeStyle = Math.round(Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)); window.__readerStyleSignature = styleSignature; rememberReaderImageAnchors(); var fontCss = ""; if (fontFamily && fontFamily !== "Original" && fontFamily !== "") { var fallback = "sans-serif"; if (fontFamily === "Merriweather" || fontFamily === "Lora") { fallback = "serif"; } else if (fontFamily === "Roboto Mono") { fallback = "monospace"; } fontCss = ` body, p, span, div, li, a, h1, h2, h3, h4, h5, h6, blockquote, td, th { font-family: '${fontFamily}', ${fallback} !important; } `; } 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 === "right") { alignCss = alignSelector + " { text-align: right !important; }"; } else if (textAlign === "justify") { alignCss = alignSelector + " { text-align: justify !important; -webkit-hyphens: auto !important; hyphens: auto !important; }"; } var gapCss = ""; if (newGap !== 1.0) { gapCss = ` body p, body ul, body ol, body blockquote { margin-top: ${newGap}em !important; margin-bottom: ${newGap}em !important; } body li { margin-bottom: ${0.5 * newGap}em !important; } `; } var sizeCss = ""; if (newFontSize !== 1.0) { sizeCss = `body { font-size: ${newFontSize}em !important; }`; } var lineHeightCss = ""; if (newLineHeight !== 1.0) { lineHeightCss = ` body, p, div, span, li, a, h1, h2, h3, h4, h5, h6, blockquote, td, th { line-height: ${newLineHeight} !important; } `; } var horizontalPaddingPx = Math.max(0, 16 * newHorizontalMargin); var verticalPaddingPx = Math.max(0, 16 * newVerticalMargin); var horizontalMarginCss = ` body { box-sizing: border-box !important; padding-left: ${horizontalPaddingPx}px !important; padding-right: ${horizontalPaddingPx}px !important; padding-top: ${verticalPaddingPx}px !important; padding-bottom: ${verticalPaddingPx}px !important; } `; var imageCss = ` :root { --reader-image-size: ${newImageSize}; } body img, body svg, body video, body canvas, body image { width: min(100%, calc(100% * var(--reader-image-size))) !important; max-width: 100% !important; height: auto !important; display: block !important; float: none !important; margin-left: auto !important; margin-right: auto !important; object-fit: contain !important; } body p:has(> img:only-child), body div:has(> img:only-child), body figure { text-align: center !important; } `; dynamicStyleElement.innerHTML = [sizeCss, lineHeightCss, fontCss, alignCss, gapCss, imageCss, horizontalMarginCss].join("\n"); applyReaderImageAnchors(); setTimeout(applyReaderImageAnchors, 80); logVerticalJitter( "jsStyleApply scrollY=" + scrollYBeforeStyle + " scrollHeight=" + scrollHeightBeforeStyle + "->" + Math.round(Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)) + " signature=" + styleSignature, ); 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; console.log(logTag + ":[PARAGRAPH] Computed font-family: " + computedPara); } 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); } } }, 300 ); if (window.triggerInitialScrollStateReport) { window.triggerInitialScrollStateReport(); } else if (window.reportScrollState) { setTimeout(window.reportScrollState, 60); } return true; }; window.TOC_FRAGMENTS = window.TOC_FRAGMENTS || []; window.setTocFragments = function (jsonArray) { var signature = JSON.stringify(jsonArray || []); if (window.__readerTocFragmentsSignature === signature) { logVerticalJitter("tocSkip unchanged count=" + ((jsonArray && jsonArray.length) || 0)); return false; } window.__readerTocFragmentsSignature = signature; console.log("FRAG_NAV_DEBUG: window.setTocFragments called with " + jsonArray.length + " items."); logVerticalJitter("tocApply count=" + jsonArray.length); window.TOC_FRAGMENTS = jsonArray; // Immediate audit and report window.auditTocFragments(); window.reportScrollState(); return true; }; var lastVerticalJitterScrollLogAt = 0; var jitterProbeLastScrollY = null; var jitterProbeLastScrollHeight = null; var jitterProbeLastTime = 0; var jitterProbeTrend = 0; var jitterProbeLastAnomalyAt = 0; var jitterProbeTouchActive = false; var jitterProbeLastTouchY = null; var jitterProbeExpectedScrollDirection = 0; function updateVerticalJitterScrollProbe(scrollY, scrollHeight, clientHeight) { var now = Date.now(); if (jitterProbeLastScrollY === null) { jitterProbeLastScrollY = scrollY; jitterProbeLastScrollHeight = scrollHeight; jitterProbeLastTime = now; return; } var deltaY = scrollY - jitterProbeLastScrollY; var deltaHeight = scrollHeight - jitterProbeLastScrollHeight; var elapsedMs = now - jitterProbeLastTime; var direction = deltaY > 0 ? 1 : deltaY < 0 ? -1 : 0; var wasOppositeToTouch = jitterProbeTouchActive && jitterProbeExpectedScrollDirection !== 0 && direction !== 0 && direction !== jitterProbeExpectedScrollDirection; var wasOppositeToTrend = !jitterProbeTouchActive && jitterProbeTrend !== 0 && direction !== 0 && direction !== jitterProbeTrend; var isSmallReverse = Math.abs(deltaY) >= 2 && Math.abs(deltaY) <= 96; var canLogAnomaly = now - jitterProbeLastAnomalyAt >= 120; if (deltaHeight !== 0 && canLogAnomaly) { logVerticalJitter( "scrollHeightChange y=" + jitterProbeLastScrollY + "->" + scrollY + " height=" + jitterProbeLastScrollHeight + "->" + scrollHeight + " touch=" + jitterProbeTouchActive, ); jitterProbeLastAnomalyAt = now; } else if ((wasOppositeToTouch || wasOppositeToTrend) && isSmallReverse && deltaHeight === 0 && canLogAnomaly) { logVerticalJitter( "scrollReverseSmall y=" + jitterProbeLastScrollY + "->" + scrollY + " dy=" + deltaY + " elapsedMs=" + elapsedMs + " expected=" + jitterProbeExpectedScrollDirection + " trend=" + jitterProbeTrend + " clientHeight=" + clientHeight, ); jitterProbeLastAnomalyAt = now; } if (direction !== 0) { jitterProbeTrend = direction; } jitterProbeLastScrollY = scrollY; jitterProbeLastScrollHeight = scrollHeight; jitterProbeLastTime = now; } 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 hasFoundVisible = false; if (window.TOC_FRAGMENTS && window.TOC_FRAGMENTS.length > 0) { var threshold = window.VIEWPORT_PADDING_TOP + 60; for (var i = 0; i < window.TOC_FRAGMENTS.length; i++) { var id = window.TOC_FRAGMENTS[i]; var el = document.getElementById(id) || document.querySelector('[name="' + id + '"]'); if (el) { hasFoundVisible = true; var rect = el.getBoundingClientRect(); 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); updateVerticalJitterScrollProbe(scrollY, scrollHeight, clientHeight); var now = Date.now(); if (now - lastVerticalJitterScrollLogAt >= 1000) { logVerticalJitter( "scrollReport y=" + scrollY + " scrollHeight=" + scrollHeight + " clientHeight=" + clientHeight + " activeFragment=" + (activeFragment || ""), ); lastVerticalJitterScrollLogAt = now; } } 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( "touchstart", function (event) { var touch = event.touches && event.touches[0]; jitterProbeTouchActive = true; jitterProbeLastTouchY = touch ? touch.clientY : null; jitterProbeExpectedScrollDirection = 0; }, { passive: true }, ); window.addEventListener( "touchmove", function (event) { var touch = event.touches && event.touches[0]; if (!touch || jitterProbeLastTouchY === null) return; var fingerDeltaY = touch.clientY - jitterProbeLastTouchY; if (Math.abs(fingerDeltaY) >= 1) { jitterProbeExpectedScrollDirection = fingerDeltaY < 0 ? 1 : -1; } jitterProbeLastTouchY = touch.clientY; }, { passive: true }, ); window.addEventListener( "touchend", function () { jitterProbeLastTouchY = null; setTimeout(function () { jitterProbeTouchActive = false; jitterProbeExpectedScrollDirection = 0; }, 250); }, { passive: true }, ); window.addEventListener( "touchcancel", function () { jitterProbeTouchActive = false; jitterProbeLastTouchY = null; jitterProbeExpectedScrollDirection = 0; }, { passive: true }, ); let scrollThrottleTimeout = null; let lastScrollTime = 0; window.addEventListener("scroll", function() { const now = Date.now(); if (now - lastScrollTime >= 100) { window.reportScrollState(); lastScrollTime = now; } else { if (scrollThrottleTimeout) clearTimeout(scrollThrottleTimeout); scrollThrottleTimeout = setTimeout(function() { window.reportScrollState(); lastScrollTime = Date.now(); }, 100); } }, { 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, '$1'); 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, true); 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 () { console.log("TTS_CHAPTER_CHANGE_DIAG: Starting extractTextWithCfiFromTop"); try { const ttsNodeSelector = "p, h1, h2, h3, h4, h5, h6, li, blockquote"; const allContentNodesRaw = Array.from(document.body.querySelectorAll(ttsNodeSelector)); const allContentNodes = allContentNodesRaw.filter(node => node.querySelector(ttsNodeSelector) === null); console.log("TTS_CHAPTER_CHANGE_DIAG: Total nodes found: " + allContentNodes.length); 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) { console.log("TTS_CHAPTER_CHANGE_DIAG: No visible start block found in viewport."); return window.extractTextWithCfi(); } console.log("TTS_CHAPTER_CHANGE_DIAG: Starting extraction from node index: " + startIndex); 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) { console.log("TTS_CHAPTER_CHANGE_DIAG: Error in extractTextWithCfiFromTop: " + e.message); 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 () { const traceId = Date.now(); console.log("TTS_CHAPTER_CHANGE_DIAG: [" + traceId + "] extractAndRelayText invoked."); try { const structuredTextJson = window.extractTextWithCfiFromTop(); const len = structuredTextJson ? structuredTextJson.length : 0; console.log("TTS_CHAPTER_CHANGE_DIAG: [" + traceId + "] Relay text JSON length: " + len); if (typeof TtsBridge !== "undefined" && TtsBridge.onStructuredTextExtracted) { TtsBridge.onStructuredTextExtracted(structuredTextJson); } else { console.log("TTS_CHAPTER_CHANGE_DIAG: [" + traceId + "] Bridge missing!"); } } catch (e) { console.log("TTS_CHAPTER_CHANGE_DIAG: [" + traceId + "] Error: " + e.message); 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 parseReaderChunkInt(value, fallback) { const parsed = parseInt(value, 10); return isNaN(parsed) ? fallback : parsed; } function getReaderChunkIndex(chunkElement) { return parseReaderChunkInt(chunkElement && chunkElement.dataset ? chunkElement.dataset.chunkIndex : null, 0); } function getReaderChunkElementStartIndex(chunkElement) { const chunkIndex = getReaderChunkIndex(chunkElement); return parseReaderChunkInt( chunkElement && chunkElement.dataset ? chunkElement.dataset.elementStartIndex : null, chunkIndex * 20 ); } function getReaderChunkElementCount(chunkElement) { return parseReaderChunkInt( chunkElement && chunkElement.dataset ? chunkElement.dataset.elementCount : null, 20 ); } function findReaderChunkForElementIndex(container, childNodeIndex) { const chunks = Array.from(container.querySelectorAll(".chunk-container")); for (let i = 0; i < chunks.length; i++) { const chunkElement = chunks[i]; const elementStartIndex = getReaderChunkElementStartIndex(chunkElement); const elementCount = getReaderChunkElementCount(chunkElement); if (elementCount <= 0) continue; if (childNodeIndex >= elementStartIndex && childNodeIndex < elementStartIndex + elementCount) { return { chunkElement: chunkElement, chunkIndex: getReaderChunkIndex(chunkElement), indexInChunk: childNodeIndex - elementStartIndex }; } } const fallbackChunkIndex = Math.floor(childNodeIndex / 20); const fallbackChunkElement = container.querySelector(`.chunk-container[data-chunk-index="${fallbackChunkIndex}"]`); if (!fallbackChunkElement) return null; return { chunkElement: fallbackChunkElement, chunkIndex: fallbackChunkIndex, indexInChunk: childNodeIndex % 20 }; } function resolveCfiPath(rootElement, path, requestChunkIfMissing = false) { 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; const chunkLookup = findReaderChunkForElementIndex(currentNode, childNodeIndex); if (!chunkLookup) return null; let chunkIndex = chunkLookup.chunkIndex; let indexInChunk = chunkLookup.indexInChunk; let chunkElement = chunkLookup.chunkElement; if (chunkElement) { if (chunkElement.innerHTML === "") { if (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); } } else { if (requestChunkIfMissing) { console.log("PosSaveDiag: Requesting missing chunk " + chunkIndex + " for CFI resolution."); if (window.ContentBridge && window.ContentBridge.requestChunk) { if (!window._requestedChunksForCfi) window._requestedChunksForCfi = {}; if (!window._requestedChunksForCfi[chunkIndex]) { window._requestedChunksForCfi[chunkIndex] = true; window.ContentBridge.requestChunk(chunkIndex); } } } return null; } } 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, requestChunkIfMissing = false) { 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, requestChunkIfMissing); 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 = getReaderChunkIndex(parentNode); let elementsInPrecedingChunks = getReaderChunkElementStartIndex(parentNode); 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); } function normalizeReaderImageSourceForMatch(value) { if (!value) return ""; var normalized = String(value).split("#")[0].split("?")[0].replace(/\\/g, "/"); try { normalized = decodeURIComponent(normalized); } catch (e) {} if (normalized.indexOf("file://") === 0) { normalized = normalized.substring("file://".length); } return normalized.toLowerCase(); } function getReaderImageSourceCandidates(element) { if (!element) return []; var values = [ element.currentSrc, element.src, element.href && element.href.baseVal, element.getAttribute && element.getAttribute("src"), element.getAttribute && element.getAttribute("href"), element.getAttribute && element.getAttribute("xlink:href"), element.getAttribute && element.getAttribute("data-src"), ]; return values.filter(function (value, index, array) { return value && array.indexOf(value) === index; }); } function readerImageCandidateMatches(candidate, normalizedTargets) { var normalizedCandidate = normalizeReaderImageSourceForMatch(candidate); if (!normalizedCandidate) return false; var candidateName = normalizedCandidate.substring(normalizedCandidate.lastIndexOf("/") + 1); return normalizedTargets.some(function (target) { if (!target) return false; var targetName = target.substring(target.lastIndexOf("/") + 1); return ( normalizedCandidate === target || normalizedCandidate.endsWith("/" + targetName) || target.endsWith("/" + candidateName) || (candidateName && targetName && candidateName === targetName) ); }); } function findReaderImageElementsBySource(source, originalSource) { var normalizedTargets = [source, originalSource] .map(normalizeReaderImageSourceForMatch) .filter(Boolean); return getReaderImageElements().filter(function (element) { return getReaderImageSourceCandidates(element).some(function (candidate) { return readerImageCandidateMatches(candidate, normalizedTargets); }); }); } function findReaderImageChunkIndex(source, originalSource) { if (!window.virtualization || !window.virtualization.chunksData) return -1; var normalizedTargets = [source, originalSource] .map(normalizeReaderImageSourceForMatch) .filter(Boolean); var targetNames = normalizedTargets .map(function (target) { return target.substring(target.lastIndexOf("/") + 1); }) .filter(Boolean); for (var i = 0; i < window.virtualization.chunksData.length; i++) { var chunkHtml = window.virtualization.chunksData[i]; if (!chunkHtml) continue; var normalizedChunk = normalizeReaderImageSourceForMatch(chunkHtml); if ( normalizedTargets.some(function (target) { return normalizedChunk.indexOf(target) !== -1; }) || targetNames.some(function (name) { return normalizedChunk.indexOf(name) !== -1; }) ) { return i; } } return -1; } window.scrollToReaderImageSource = function(source, ordinal, originalSource) { var safeOrdinal = Math.max(0, parseInt(ordinal || 0, 10) || 0); var matches = findReaderImageElementsBySource(source, originalSource); if (!matches.length) { var chunkIndex = findReaderImageChunkIndex(source, originalSource); if (chunkIndex >= 0) { var chunkDiv = document.querySelector('.chunk-container[data-chunk-index="' + chunkIndex + '"]'); if (chunkDiv && chunkDiv.innerHTML === "" && window.virtualization && window.virtualization.chunksData[chunkIndex]) { chunkDiv.innerHTML = window.virtualization.chunksData[chunkIndex]; chunkDiv.style.height = ""; } matches = findReaderImageElementsBySource(source, originalSource); } } var target = matches[Math.min(safeOrdinal, Math.max(0, matches.length - 1))]; if (!target) return false; var rect = target.getBoundingClientRect(); var targetScrollY = window.scrollY + rect.top - (window.VIEWPORT_PADDING_TOP + 10); window.scrollTo({ top: Math.max(0, targetScrollY), behavior: "auto" }); setTimeout(function () { if (window.reportScrollState) window.reportScrollState(); }, 80); return true; }; 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); window._requestedChunksForCfi = {}; // Reset the requested cache if (!cleanCfi || !cleanCfi.startsWith('/')) { if (window.CfiBridge && window.CfiBridge.onScrollFinished) { window.CfiBridge.onScrollFinished(false); } return; } let attempts = 0; const maxAttempts = 40; // Increased to 40 attempts (4 seconds) to give Kotlin time to inject chunks let stabilizingFrames = 0; const maxStabilizingFrames = 8; function attemptScroll() { attempts++; try { // Pass 'true' to dynamically request any missing chunks needed to resolve the position const location = window.getNodeAndOffsetFromCfi(cleanCfi, true); 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); const endOffset = Math.min(validOffset + 1, currentNode.nodeValue.length); if (validOffset < endOffset) { range.setStart(currentNode, validOffset); range.setEnd(currentNode, endOffset); } else if (validOffset > 0) { range.setStart(currentNode, validOffset - 1); range.setEnd(currentNode, validOffset); } else { range.setStart(currentNode, validOffset); range.collapse(true); } let rect = range.getBoundingClientRect(); const rects = range.getClientRects(); if (rects && rects.length > 0) { rect = rects[0]; } 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 { console.log("PosSaveDiag: attemptScroll failed after " + maxAttempts + " attempts for CFI: " + cleanCfi); if (window.CfiBridge && window.CfiBridge.onScrollFinished) { window.CfiBridge.onScrollFinished(false); } } } } catch (e) { console.log("PosSaveDiag: attemptScroll exception: " + e.message); 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; const logVerticalJitter = window.logReaderVerticalJitter || function (message) { console.log("EpubVerticalJitter: " + message); }; 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}`); logVerticalJitter( "virtInit total=" + total + " anchor=" + initialChunkIndex + " scrollY=" + Math.round(window.scrollY || window.pageYOffset || 0) + " scrollHeight=" + Math.round(Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)), ); 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]) { logVerticalJitter("virtRequestChunk idx=" + 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); } logVerticalJitter( "virtRestore idx=" + idx + " oldHeight=" + Math.round(oldHeight) + " newHeight=" + Math.round(newHeight) + " top=" + Math.round(div.getBoundingClientRect().top) + " pendingAdjust=" + Math.round(scrollAdjust), ); 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; logVerticalJitter( "virtUnload idx=" + idx + " placeholderHeight=" + Math.round(oldHeight) + " top=" + Math.round(div.getBoundingClientRect().top), ); } } }); if (scrollAdjust !== 0) { let before = Math.round(window.scrollY || window.pageYOffset || 0); window.scrollBy(0, scrollAdjust); logVerticalJitter( "virtScrollAdjust dy=" + Math.round(scrollAdjust) + " scrollY=" + before + "->" + Math.round(window.scrollY || window.pageYOffset || 0), ); } 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`); logVerticalJitter( "virtAppend idx=" + index + " existingData=" + Boolean(Array.isArray(this.chunksData) && this.chunksData[index]) + " scrollY=" + Math.round(window.scrollY || window.pageYOffset || 0), ); 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) { let before = Math.round(window.scrollY || window.pageYOffset || 0); window.scrollBy(0, newHeight - oldHeight); logVerticalJitter( "virtAppendScrollAdjust idx=" + index + " dy=" + Math.round(newHeight - oldHeight) + " scrollY=" + before + "->" + Math.round(window.scrollY || window.pageYOffset || 0), ); } 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); } setTimeout(applyReaderImageAnchors, 80); }, }; 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)); }, }; })();