Download static/reader/core/reader-bindings.mjs from VoiceOfML/Search: direct link, hf CLI and curl.
- Browser
- Download file 41.5 kB
-
https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader/core/reader-bindings.mjs
- Command line
-
hf download hf://spaces/VoiceOfML/Search/static/reader/core/reader-bindings.mjs
-
curl -L -o reader-bindings.mjs https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader/core/reader-bindings.mjs
41.5 kB
| export function createReaderBindings(readerContext) { | |
| function trackStartupOwners() { | |
| readerContext.readerRuntime.track(readerContext.readerRequestManager); | |
| readerContext.readerRuntime.track(readerContext.formatAdapters); | |
| readerContext.readerRuntime.track(window.__VOICE_PDF_PRELOAD__); | |
| readerContext.readerRuntime.track(window.__VOICE_READER_CHAPTER_PRELOAD__); | |
| } | |
| function trackWorkCancellation() { | |
| readerContext.readerRuntime.track(readerContext.stopReaderWork); | |
| } | |
| function trackEarlySourceResolution() { | |
| readerContext.trackReaderResource(() => window.__VOICE_READER_RESOLVE__?.dispose()); | |
| } | |
| function publishDiagnosticsAndInstallCompatibility() { | |
| window.VoiceOfMLReaderPdfDiagnostics = readerContext.pdfDiagnostics; | |
| readerContext.trackReaderResource(() => { | |
| readerContext.pdfPageTimings.length = 0; | |
| if (window.VoiceOfMLReaderPdfDiagnostics === readerContext.pdfDiagnostics) delete window.VoiceOfMLReaderPdfDiagnostics; | |
| }); | |
| // Polyfills for vendor engines (pdf.js, foliate): neither helper has a direct | |
| // caller in first-party code, but both are required by pinned vendor bundles. | |
| if (!Map.prototype.getOrInsertComputed) { | |
| Map.prototype.getOrInsertComputed = function (key, callback) { | |
| if (this.has(key)) return this.get(key); | |
| const value = callback(key); | |
| this.set(key, value); | |
| return value; | |
| }; | |
| } | |
| if (!Math.sumPrecise) { | |
| Math.sumPrecise = function (values) { | |
| let sum = 0; | |
| let correction = 0; | |
| for (const value of values) { | |
| const next = sum + value; | |
| correction += Math.abs(sum) >= Math.abs(value) ? sum - next + value : value - next + sum; | |
| sum = next; | |
| } | |
| return sum + correction; | |
| }; | |
| } | |
| } | |
| function readSelectedSourceSession() { | |
| try { | |
| readerContext.localReaderData = JSON.parse(sessionStorage.getItem(`reader-source:${readerContext.readerId}`) || "null"); | |
| if (readerContext.localReaderData) sessionStorage.removeItem(`reader-source:${readerContext.readerId}`); | |
| } catch (_) {} | |
| } | |
| function normalizeConvertedPdfMode() { | |
| if ( | |
| readerContext.extension === "pdf" && | |
| /\/api\/reader-bucket-resource\?/.test(readerContext.sourceUrl) && | |
| new URL(readerContext.sourceUrl, location.href).searchParams.get("path")?.endsWith("page-manifest.json") | |
| ) { | |
| readerContext.extension = "pdf-pages"; | |
| readerContext.capability = readerContext.readerRuntime.negotiate(VoiceOfMLReader.capability(readerContext.extension)); | |
| } | |
| } | |
| function applySourceSessionAndCachedMetadata() { | |
| if (readerContext.localReaderData) { | |
| if (readerContext.localReaderData.title) readerContext.params.set("title", readerContext.localReaderData.title); | |
| if (readerContext.localReaderData.extension) readerContext.params.set("ext", readerContext.localReaderData.extension); | |
| if (readerContext.localReaderData.repo) | |
| readerContext.params.set("path", [readerContext.localReaderData.repo, ...(readerContext.localReaderData.folder || [])].join("/")); | |
| } | |
| try { | |
| const cached = sessionStorage.getItem(`reader-resolve:${readerContext.readerId}`); | |
| if (cached) { | |
| readerContext.cachedReaderData = JSON.parse(cached); | |
| readerContext.resolvedReaderData = readerContext.cachedReaderData; | |
| sessionStorage.removeItem(`reader-resolve:${readerContext.readerId}`); | |
| } | |
| } catch (_) {} | |
| } | |
| function publishSourceAndSubscribeToStore() { | |
| readerContext.readerRuntime.update("source", { | |
| id: readerContext.readerId, | |
| url: readerContext.sourceUrl, | |
| contentUrl: readerContext.contentUrl, | |
| downloadUrl: readerContext.downloadUrl, | |
| extension: readerContext.extension, | |
| metadata: readerContext.resolvedReaderData | |
| }); | |
| readerContext.readerRuntime.events.on("phase", ({ phase }) => { | |
| document.documentElement.dataset.readerPhase = phase; | |
| }); | |
| readerContext.readerRuntime.track( | |
| VoiceOfMLReaderStore.subscribe?.((change) => { | |
| if (!change?.remote || readerContext.readerLifecycle.disposed) return; | |
| if ( | |
| change.type === "history-clear" || | |
| (change.type === "history-remove" && change.url === readerContext.sourceUrl) | |
| ) { | |
| readerContext.historySuppressed = true; | |
| readerContext.readerRuntime.cancel(readerContext.saveTimer); | |
| readerContext.saveTimer = readerContext.saveDeadline = 0; | |
| } | |
| if ( | |
| ["history", "history-clear", "history-remove"].includes(change.type) && | |
| !document.querySelector("#history-panel").hidden | |
| ) | |
| readerContext.renderHistory(); | |
| if ( | |
| ["bookmark", "bookmark-remove"].includes(change.type) && | |
| (readerContext.panelState.showingAllBookmarks || !change.url || change.url === readerContext.sourceUrl) && | |
| !document.querySelector("#bookmarks-panel").hidden | |
| ) | |
| readerContext.renderBookmarks(); | |
| }) | |
| ); | |
| readerContext.setReaderPhase("startup"); | |
| window.fetchFile = async (url) => { | |
| const requestUrl = String(url) === readerContext.sourceUrl ? readerContext.contentUrl : url; | |
| const response = await readerContext.readerRequestManager.request(requestUrl, readerContext.READER_PROXY_TIMEOUT_MS); | |
| if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); | |
| const fileName = new URL(url, location.href).pathname.split("/").pop() || "book"; | |
| return new File( | |
| [ | |
| await VoiceOfMLReaderSecurity.readBytes( | |
| response, | |
| VoiceOfMLReaderSecurity.LIMITS.archiveCompressedBytes | |
| ) | |
| ], | |
| fileName, | |
| { type: response.headers.get("content-type") || "application/octet-stream" } | |
| ); | |
| }; | |
| } | |
| function initializePanelAccessibility() { | |
| document.querySelectorAll('.reader-toolbar svg, #history-panel > header svg').forEach(node => { | |
| node.setAttribute('aria-hidden', 'true'); node.setAttribute('focusable', 'false'); | |
| }); | |
| document.querySelector(".reader-panel-tabs")?.setAttribute("role", "tablist"); | |
| document.querySelector(".reader-skip-link")?.addEventListener("click", (event) => { | |
| event.preventDefault(); | |
| readerContext.setReaderPanelOpen(false); | |
| readerContext.viewport.focus({ preventScroll: true }); | |
| }); | |
| } | |
| function mountInitialLoadingFeedback() { | |
| readerContext.loadingIndicator.className = "reader-loading-indicator"; | |
| readerContext.loadingIndicator.setAttribute("role", "status"); | |
| readerContext.loadingIndicator.innerHTML = | |
| '<span class="reader-loading-spinner" aria-hidden="true"></span><span>正在加载正文...</span>'; | |
| readerContext.content.appendChild(readerContext.loadingIndicator); | |
| readerContext.content.dataset.mode = readerContext.capability.mode || "unsupported"; | |
| } | |
| function bindDownloadAction() { | |
| readerContext.downloadButton.addEventListener("click", readerContext.startReaderDownload); | |
| readerContext.loadingStatus.textContent = ""; | |
| } | |
| function initializeDocumentTitle() { | |
| let savedZoom = null; | |
| try { savedZoom = localStorage.getItem("reader-zoom"); } catch (_) {} | |
| readerContext.updateDocumentState({ | |
| title: | |
| String( | |
| readerContext.params.get("title") || (readerContext.localReaderData && readerContext.localReaderData.title) || readerContext.sourceName | |
| ).trim() || readerContext.sourceName, | |
| zoom: Math.min(4, Math.max(0.25, Number(savedZoom || 100) / 100)) | |
| }); | |
| } | |
| function restoreReturnNavigation() { | |
| try { | |
| const state = history.state; | |
| const saved = JSON.parse(sessionStorage.getItem("reader-navigation-current") || "null"); | |
| let stateUrl = | |
| state && state.voiceReaderOverlay && state.readerUrl | |
| ? new URL(state.readerUrl, location.origin) | |
| : null; | |
| if (!stateUrl && saved && saved.shareUrl === location.href && saved.readerUrl) | |
| stateUrl = new URL(saved.readerUrl, location.origin); | |
| const cleanStateUrl = stateUrl && new URL(stateUrl.href); | |
| if (cleanStateUrl) { | |
| cleanStateUrl.searchParams.delete("return"); | |
| cleanStateUrl.searchParams.delete("nav"); | |
| } | |
| if ( | |
| stateUrl && | |
| stateUrl.origin === location.origin && | |
| stateUrl.pathname === "/static/reader.html" && | |
| cleanStateUrl.href === location.href | |
| ) { | |
| if (!readerContext.returnUrl) readerContext.returnUrl = stateUrl.searchParams.get("return") || ""; | |
| if (!readerContext.returnNavigationToken) readerContext.returnNavigationToken = stateUrl.searchParams.get("nav") || ""; | |
| readerContext.returnNeedsReload = true; | |
| } | |
| } catch (_) {} | |
| } | |
| function validateReturnHistory() { | |
| try { | |
| const target = new URL(readerContext.returnUrl, location.origin); | |
| const storedReturnUrl = readerContext.returnHistoryKey ? sessionStorage.getItem(readerContext.returnHistoryKey) : ""; | |
| readerContext.canReturnWithHistory = !!readerContext.returnHistoryKey && storedReturnUrl === target.href; | |
| } catch (_) {} | |
| } | |
| function ownChapterSearchClient() { | |
| readerContext.trackReaderResource(() => { | |
| readerContext.chapterSearchClient?.dispose(); | |
| readerContext.chapterSearchClient = null; | |
| }); | |
| } | |
| function ownIndependentPdfResources() { | |
| readerContext.trackReaderResource(() => { | |
| for (const release of [...readerContext.pdfV3BlobReleases]) release(); | |
| readerContext.pdfV3Repository?.dispose(); readerContext.pdfTextLayerRepository?.dispose(); | |
| }); | |
| } | |
| function ownPdfBookSearchClient() { | |
| readerContext.trackReaderResource(() => { | |
| readerContext.pdfBookSearchClient?.dispose(); | |
| readerContext.pdfBookSearchClient = null; | |
| clearTimeout(readerContext.pdfBookSearchPrefetchTimer); | |
| readerContext.pdfBookSearchPrefetchTimer = 0; | |
| readerContext.pdfBookSearchPrefetchPromise = null; | |
| }); | |
| } | |
| function ownPdfPrefetchAndQueues() { | |
| readerContext.trackReaderResource(readerContext.clearPdfManifestPrefetches); | |
| readerContext.trackReaderResource(() => { | |
| for (const waiter of readerContext.pdfRenderWaiters.splice(0)) waiter.reject(readerContext.readerAbortError()); | |
| for (const waiter of readerContext.pdfTextWaiters.splice(0)) waiter.reject(readerContext.readerAbortError()); | |
| }); | |
| } | |
| function initializeGenerations() { | |
| for (const generationName of ["navigation", "search", "bookmarks", "history", "pdf"]) | |
| readerContext.nextReaderGeneration(generationName); | |
| } | |
| function labelToolbarControls() { | |
| for (const [selector, label] of [ | |
| ["#back", "返回"], | |
| ["#page-prev", "上一页"], | |
| ["#page-next", "下一页"], | |
| ["#page-number", "页码"], | |
| ["#zoom-out", "缩小"], | |
| ["#zoom-in", "放大"], | |
| ["#zoom", "缩放百分比"], | |
| ["#bookmark-ribbon", "添加书签"], | |
| ["#history", "阅读选项"] | |
| ]) | |
| document.querySelector(selector)?.setAttribute("aria-label", label); | |
| } | |
| function applyMetadataAndWatchSanitizedContent() { | |
| readerContext.applyReaderMetadata(readerContext.resolvedReaderData); | |
| readerContext.contentSanitizerObserver = new MutationObserver(() => { | |
| const seen = new Set(); | |
| for (const article of document.querySelectorAll(".foliate-continuous > article[data-section]")) { | |
| article.querySelectorAll("style,link[rel=stylesheet]").forEach((node) => node.remove()); | |
| const index = article.dataset.section; | |
| if (seen.has(index)) article.remove(); | |
| else seen.add(index); | |
| } | |
| }); | |
| readerContext.contentSanitizerObserver.observe(readerContext.content, { childList: true, subtree: true }); | |
| readerContext.bookmarkPopover.className = "bookmark-popover"; | |
| readerContext.bookmarkPopover.setAttribute("role", "dialog"); | |
| readerContext.bookmarkPopover.setAttribute("aria-modal", "true"); | |
| readerContext.bookmarkPopover.setAttribute("aria-labelledby", "bookmark-prompt"); | |
| } | |
| function initializeBookmarkEditor() { | |
| readerContext.bookmarkLabelInput.id = "bookmark-label"; | |
| readerContext.bookmarkLabelInput.maxLength = 120; | |
| readerContext.bookmarkExcerptInput.id = "bookmark-excerpt-input"; | |
| readerContext.bookmarkExcerptInput.maxLength = 500; | |
| readerContext.bookmarkExcerptInput.rows = 3; | |
| readerContext.bookmarkEditFields.className = "bookmark-edit-fields"; | |
| readerContext.bookmarkEditFields.innerHTML = "<label>标题</label><label>摘要</label>"; | |
| readerContext.bookmarkEditFields.children[0].appendChild(readerContext.bookmarkLabelInput); | |
| readerContext.bookmarkEditFields.children[1].appendChild(readerContext.bookmarkExcerptInput); | |
| readerContext.bookmarkPopover.insertBefore(readerContext.bookmarkEditFields, readerContext.bookmarkPopover.querySelector("div")); | |
| } | |
| function initializeBookmarkSearchControls() { | |
| if (!document.querySelector("#bookmarks-panel .panel-search-toggle")) { | |
| const button = document.createElement("button"); | |
| button.className = "panel-search-toggle"; | |
| button.type = "button"; | |
| document.querySelector("#bookmarks-panel .panel-view-header").appendChild(button); | |
| } | |
| readerContext.bookmarksAllButton.id = "bookmarks-all"; | |
| readerContext.bookmarksAllButton.className = "text-action"; | |
| readerContext.bookmarksAllButton.type = "button"; | |
| readerContext.bookmarksAllButton.textContent = "全部书签"; | |
| readerContext.bookmarksAllButton.setAttribute("aria-pressed", "false"); | |
| } | |
| function mountBookmarkHeaderActions() { | |
| readerContext.bookmarksHeaderActions.append(readerContext.bookmarksAllButton, readerContext.bookmarksSearchButton); | |
| readerContext.bookmarksHeader.appendChild(readerContext.bookmarksHeaderActions); | |
| } | |
| function mountMediaPanel() { | |
| readerContext.mediaTab.id = "media-tab"; | |
| readerContext.mediaTab.type = "button"; | |
| readerContext.mediaTab.role = "tab"; | |
| readerContext.mediaTab.dataset.panel = "media"; | |
| readerContext.mediaTab.textContent = "播放"; | |
| readerContext.mediaTab.hidden = !readerContext.capability.features.media; | |
| readerContext.mediaPanel.id = "media-panel"; | |
| readerContext.mediaPanel.className = "reader-panel-view"; | |
| readerContext.mediaPanel.dataset.panelView = "media"; | |
| readerContext.mediaPanel.hidden = true; | |
| readerContext.mediaPanel.innerHTML = | |
| '<div class="media-panel-content"><strong>播放状态</strong><span class="media-panel-time">尚未播放</span><button class="text-action media-panel-bookmark" type="button">在当前时间添加书签</button></div>'; | |
| document.querySelector(".reader-panel-tabs").prepend(readerContext.mediaTab); | |
| document | |
| .querySelector("#history-panel") | |
| .insertBefore(readerContext.mediaPanel, document.querySelector("#toc-panel")); | |
| readerContext.mediaPanel | |
| .querySelector(".media-panel-bookmark") | |
| .addEventListener("click", (event) => readerContext.openBookmarkPopover(event.currentTarget)); | |
| document.querySelector(".page-controls").hidden = !readerContext.capability.features.pagination; | |
| document.querySelector(".zoom-controls").hidden = !readerContext.capability.features.zoom; | |
| readerContext.status.hidden = true; | |
| readerContext.mediaTab.setAttribute("aria-controls", "media-panel"); | |
| readerContext.mediaTab.setAttribute("aria-selected", "false"); | |
| readerContext.mediaPanel.setAttribute("role", "tabpanel"); | |
| readerContext.mediaPanel.setAttribute("aria-labelledby", "media-tab"); | |
| } | |
| function restoreAndBindPageInversion() { | |
| try { readerContext.pageInverted = localStorage.getItem("reader-page-inversion") === "true"; } catch (_) {} | |
| readerContext.pageInvertToggle.addEventListener("click", () => { | |
| readerContext.pageInverted = !readerContext.pageInverted; | |
| readerContext.applyPageInversion(true); | |
| }); | |
| readerContext.applyPageInversion(); | |
| } | |
| function restoreReaderThemeAndBindReturn() { | |
| readerContext.readerThemeToggle.className = "theme-toggle"; | |
| readerContext.applyReaderTheme(readerContext.readerTheme, false, false); | |
| readerContext.readerPath.addEventListener("click", readerContext.navigateReaderFolder); | |
| document.querySelector("#back").addEventListener("click", async () => { | |
| clearTimeout(readerContext.saveTimer); | |
| await Promise.race([readerContext.saveProgress(), new Promise((resolve) => setTimeout(resolve, 300))]); | |
| if (readerContext.isReaderOverlayFrame()) { | |
| window.parent.postMessage({ type: "voice-reader-close" }, location.origin); | |
| return; | |
| } | |
| readerContext.clearReturnNavigation(); | |
| try { | |
| const target = new URL(readerContext.returnUrl, location.origin); | |
| if (target.origin === location.origin) { | |
| if (readerContext.returnNeedsReload) location.replace(target.href); | |
| else if (readerContext.canReturnWithHistory && history.length > 1) history.back(); | |
| else location.assign(target.href); | |
| return; | |
| } | |
| } catch (_) {} | |
| location.assign("/"); | |
| }); | |
| // Replay a Back tap made before this handler was bound. | |
| if (window.__VOICE_READER_EARLY_BACK__?.()) document.querySelector("#back").click(); | |
| } | |
| function bindZoomAndPageControls() { | |
| readerContext.setZoom(readerContext.documentState.zoom * 100, false); | |
| for (const [id, delta] of [ | |
| ["#zoom-out", -10], | |
| ["#zoom-in", 10] | |
| ]) { | |
| document.querySelector(id).addEventListener("click", () => { | |
| readerContext.setZoom(Number(readerContext.zoomInput.value) + delta); | |
| }); | |
| } | |
| readerContext.zoomInput.addEventListener("change", () => readerContext.setZoom(readerContext.zoomInput.value)); | |
| readerContext.zoomInput.addEventListener("keydown", (event) => { | |
| if (event.key === "Enter") { | |
| readerContext.setZoom(readerContext.zoomInput.value); | |
| readerContext.zoomInput.blur(); | |
| } | |
| }); | |
| readerContext.pageInput.addEventListener("change", () => readerContext.goToPage(readerContext.pageInput.value)); | |
| readerContext.pageInput.addEventListener("keydown", (event) => { | |
| if (event.key === "Enter") { | |
| readerContext.goToPage(readerContext.pageInput.value); | |
| readerContext.pageInput.blur(); | |
| } | |
| }); | |
| document.addEventListener("keydown", readerContext.handleReaderKeydown); | |
| document | |
| .querySelector("#page-prev") | |
| .addEventListener("click", () => | |
| readerContext.documentState.pageCount ? readerContext.goToPage(readerContext.documentState.page - 1) : readerContext.turnViewport(-1) | |
| ); | |
| document | |
| .querySelector("#page-next") | |
| .addEventListener("click", () => | |
| readerContext.documentState.pageCount ? readerContext.goToPage(readerContext.documentState.page + 1) : readerContext.turnViewport(1) | |
| ); | |
| } | |
| function bindReaderUiHistory() { | |
| // System back always leaves the Reader, exactly like its Back button. Do not | |
| // add a guard entry or re-push one from popstate: entries created without a | |
| // user activation make Chromium skip this document on the next back gesture, | |
| // landing on an older repository/book instead of the page that opened it. | |
| } | |
| function bindPanelBookmarkAndLifecycleControls() { | |
| document.addEventListener("click", readerContext.handleReaderOutsideClick); | |
| readerContext.trackReaderResource(() => document.removeEventListener("click", readerContext.handleReaderOutsideClick)); | |
| readerContext.trackReaderResource(() => { readerContext.headingLocationCache?.dispose(); readerContext.headingLocationCache = null; }); | |
| document.addEventListener("click", async (event) => { | |
| const path = event.composedPath(), | |
| link = path.find((node) => node instanceof Element && node.matches?.("a[href]")), | |
| article = path.find( | |
| (node) => | |
| node instanceof Element && node.matches?.(".foliate-continuous article[data-section]") | |
| ); | |
| if (!link || !article || !readerContext.epubRendition?.book) return; | |
| const raw = link.getAttribute("href") || ""; | |
| if (raw.startsWith("/api/reader-resource?")) return; | |
| if (!raw || readerContext.epubRendition.book.isExternal?.(raw) || /^(?:https?:|mailto:|tel:)/i.test(raw)) | |
| return; | |
| event.preventDefault(); | |
| const generation = readerContext.beginReaderNavigation(), | |
| index = Number(article.dataset.section), | |
| section = readerContext.epubRendition.book.sections.filter((item) => item.linear !== "no")[index]; | |
| let href = raw; | |
| try { | |
| href = section?.resolveHref?.(raw) || raw; | |
| } catch (_) {} | |
| try { | |
| await readerContext.activateFoliateTocEntry({ href }, generation); | |
| } catch (error) { | |
| readerContext.reportNavigationError(error, generation); | |
| } | |
| }); | |
| // Bookmark dialog owns focus and temporarily makes only its siblings inert. | |
| readerContext.bookmarkRibbon.addEventListener("click", () => readerContext.openBookmarkPopover(readerContext.bookmarkRibbon)); | |
| document.querySelector("#bookmark-cancel").addEventListener("click", readerContext.closeBookmarkPopover); | |
| document.querySelector("#bookmark-add").addEventListener("click", async (event) => { | |
| const button = event.currentTarget, | |
| editing = readerContext.panelState.editingBookmark; | |
| const snapshot = readerContext.pendingBookmarkSnapshot || editing || readerContext.captureBookmarkSnapshot(); | |
| const label = readerContext.bookmarkLabelInput.value.trim() || snapshot.label, | |
| excerpt = readerContext.bookmarkExcerptInput.value.trim(); | |
| button.disabled = true; | |
| try { | |
| await VoiceOfMLReaderStore.putBookmark( | |
| editing | |
| ? { ...editing, label, excerpt } | |
| : { | |
| ...snapshot, | |
| id: `${readerContext.sourceUrl}\0${snapshot.locator}`, | |
| url: readerContext.sourceUrl, | |
| title: readerContext.documentState.title, | |
| extension: readerContext.extension, | |
| readerUrl: readerContext.currentReaderUrl().href, | |
| label, | |
| excerpt, | |
| createdAt: Date.now() | |
| } | |
| ); | |
| const invoker = readerContext.bookmarkInvoker; | |
| readerContext.closeBookmarkPopover(); | |
| if (!document.querySelector("#bookmarks-panel").hidden) { | |
| await readerContext.renderBookmarks(); | |
| if (editing && invoker && !invoker.isConnected && document.activeElement === document.body) | |
| ( | |
| document.querySelector( | |
| `#bookmarks-list [data-bookmark-id="${CSS.escape(editing.id)}"] .panel-item-edit` | |
| ) || readerContext.bookmarksAllButton | |
| ).focus(); | |
| } | |
| } catch (_) { | |
| document.querySelector("#bookmark-prompt").textContent = "书签保存失败,请重试。"; | |
| } finally { | |
| button.disabled = false; | |
| } | |
| }); | |
| readerContext.bookmarkPopover.addEventListener("keydown", (event) => { | |
| if (event.key === "Escape") { | |
| event.preventDefault(); | |
| readerContext.closeBookmarkPopover(); | |
| return; | |
| } | |
| if (event.key !== "Tab") return; | |
| const controls = [ | |
| ...readerContext.bookmarkPopover.querySelectorAll("input,textarea,select,button,a[href],[tabindex]") | |
| ].filter((node) => !node.disabled && node.tabIndex >= 0 && node.getClientRects().length); | |
| const first = controls[0], | |
| last = controls[controls.length - 1]; | |
| if (event.shiftKey && document.activeElement === first) { | |
| event.preventDefault(); | |
| last.focus(); | |
| } else if (!event.shiftKey && document.activeElement === last) { | |
| event.preventDefault(); | |
| first.focus(); | |
| } | |
| }); | |
| for (const button of document.querySelectorAll(".reader-panel-tabs button")) | |
| button.addEventListener("click", () => readerContext.selectPanel(button.dataset.panel)); | |
| document.querySelector(".reader-panel-tabs").addEventListener("keydown", (event) => { | |
| if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return; | |
| const tabs = [...document.querySelectorAll(".reader-panel-tabs button:not([hidden])")], | |
| current = tabs.indexOf(document.activeElement); | |
| if (current < 0) return; | |
| event.preventDefault(); | |
| const next = | |
| event.key === "Home" | |
| ? 0 | |
| : event.key === "End" | |
| ? tabs.length - 1 | |
| : (current + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length; | |
| readerContext.selectPanel(tabs[next].dataset.panel); | |
| tabs[next].focus(); | |
| }); | |
| readerContext.bookmarksAllButton.addEventListener("click", () => { | |
| readerContext.updatePanelState({ showingAllBookmarks: !readerContext.panelState.showingAllBookmarks }); | |
| readerContext.bookmarksAllButton.textContent = readerContext.panelState.showingAllBookmarks ? "本书书签" : "全部书签"; | |
| readerContext.bookmarksAllButton.setAttribute("aria-pressed", String(readerContext.panelState.showingAllBookmarks)); | |
| readerContext.renderBookmarks(); | |
| }); | |
| for (const button of document.querySelectorAll(".panel-search-toggle")) { | |
| button.classList.remove("icon-button"); | |
| button.classList.add("text-action"); | |
| button.textContent = "搜索"; | |
| button.hidden = false; | |
| button.setAttribute("aria-expanded", "false"); | |
| const view = button.closest(".reader-panel-view"), input = view.querySelector(".panel-search"); | |
| input.id ||= `${view.id}-search`; | |
| button.setAttribute("aria-controls", input.id); | |
| button.setAttribute("aria-label", `搜索${view.querySelector(".panel-view-header strong")?.textContent || "列表"}`); | |
| button.addEventListener("click", () => | |
| readerContext.setPanelSearchOpen(button, button.getAttribute("aria-expanded") !== "true") | |
| ); | |
| } | |
| for (const input of document.querySelectorAll(".panel-search")) { | |
| const view = input.closest(".reader-panel-view"); | |
| input.type = "search"; | |
| input.setAttribute( | |
| "aria-label", | |
| `搜索${view.querySelector(".panel-view-header strong")?.textContent || "列表"}` | |
| ); | |
| input.addEventListener("input", (event) => { | |
| if (!event.isComposing) readerContext.filterPanel(view); | |
| }); | |
| input.addEventListener("compositionend", () => readerContext.filterPanel(view)); | |
| } | |
| document.querySelector("#history-clear").addEventListener("click", async () => { | |
| if (!confirm("清空全部阅读历史?")) return; | |
| readerContext.historySuppressed = true; | |
| await readerContext.progressSaveChain; | |
| await VoiceOfMLReaderStore.clearHistory(); | |
| readerContext.renderHistory(); | |
| }); | |
| readerContext.readerThemeToggle.addEventListener("click", () => { | |
| const theme = readerContext.readerTheme === "dark" ? "light" : "dark"; | |
| readerContext.applyReaderTheme(theme); | |
| if (readerContext.isReaderOverlayFrame()) | |
| window.parent.postMessage({ type: "voice-reader-theme", theme }, location.origin); | |
| }); | |
| window.addEventListener("storage", (event) => { | |
| if (event.key === "theme" && event.newValue) readerContext.applyReaderTheme(event.newValue, false); | |
| }); | |
| window.addEventListener("voice-reader-dispose", () => { | |
| readerContext.saveProgress(); | |
| readerContext.disposeReader(); | |
| }); | |
| window.addEventListener("message", (event) => { | |
| if ( | |
| event.origin === location.origin && | |
| event.source === window.parent && | |
| event.data?.type === "voice-reader-abort" | |
| ) { | |
| readerContext.saveProgress(); | |
| readerContext.disposeReader(); | |
| return; | |
| } | |
| if ( | |
| event.origin === location.origin && | |
| event.source === window.parent && | |
| event.data && | |
| event.data.type === "voice-reader-theme-state" | |
| ) | |
| readerContext.applyReaderTheme(event.data.theme, false); | |
| }); | |
| document | |
| .querySelector("#history") | |
| .addEventListener("click", (event) => | |
| readerContext.setReaderPanelOpen(document.querySelector("#history").getAttribute("aria-expanded") !== "true", false, event.detail === 0) | |
| ); | |
| document | |
| .querySelector("#history-close") | |
| .addEventListener("click", () => readerContext.setReaderPanelOpen(false, true)); | |
| readerContext.viewport.addEventListener("scroll", readerContext.handleReaderPositionChange, { passive: true }); | |
| for (const type of ["wheel", "touchstart", "pointerdown"]) | |
| readerContext.viewport.addEventListener(type, readerContext.notePdfScrollIntent, { passive: true }); | |
| } | |
| function bindPdfPointerSelection() { | |
| readerContext.installReaderTapSelectionClear(readerContext.viewport); | |
| readerContext.viewport.addEventListener("pointerdown", (event) => { | |
| if (event.pointerType !== "mouse") { | |
| readerContext.pdfSelectionPointer = null; | |
| return; | |
| } | |
| const run = event.target.closest?.(".reader-pdf-text-run"); | |
| const selection = document.getSelection(); | |
| const selectedRun = !selection?.isCollapsed && event.target.closest?.(".reader-page") | |
| ? selection.anchorNode?.parentElement?.closest(".reader-pdf-text-run") : null; | |
| const origin = run || selectedRun; | |
| const caret = !event.shiftKey && event.button === 0 ? readerContext.independentTextCaret(document, event.clientX, event.clientY) : null; | |
| readerContext.pdfSelectionPointer = origin | |
| ? { id: event.pointerId, x: event.clientX, y: event.clientY, frame: 0, | |
| page: origin.closest(".reader-page"), anchorNode: caret?.node || (run ? null : selection.anchorNode), | |
| anchorOffset: caret?.offset ?? (run ? 0 : selection.anchorOffset) } : null; | |
| }, { passive: true }); | |
| document.addEventListener("pointermove", (event) => { | |
| const start = readerContext.pdfSelectionPointer; | |
| if (event.pointerType !== "mouse" || !start || start.id !== event.pointerId || | |
| Math.hypot(event.clientX - start.x, event.clientY - start.y) < 6) return; | |
| const selection = document.getSelection(); | |
| if (!start.anchorNode && selection?.rangeCount && start.page.contains(selection.anchorNode) && | |
| selection.anchorNode.parentElement?.closest(".reader-pdf-text-run")) { | |
| start.anchorNode = selection.anchorNode; | |
| start.anchorOffset = selection.anchorOffset; | |
| } | |
| if (start.anchorNode) { | |
| start.endX = event.clientX; | |
| start.endY = event.clientY; | |
| if (!start.frame) start.frame = requestAnimationFrame(() => { | |
| start.frame = 0; | |
| if (readerContext.pdfSelectionPointer === start) | |
| readerContext.correctPdfCrossPageSelection(start.endX, start.endY, start); | |
| }); | |
| } | |
| }, { passive: true }); | |
| document.addEventListener("pointerup", (event) => { | |
| const start = readerContext.pdfSelectionPointer; | |
| readerContext.pdfSelectionPointer = null; | |
| if (event.pointerType !== "mouse" || !["pdf", "pdf-pages"].includes(readerContext.capability.mode) || | |
| !start || start.id !== event.pointerId || | |
| Math.hypot(event.clientX - start.x, event.clientY - start.y) < 6) return; | |
| const x = event.clientX, y = event.clientY; | |
| requestAnimationFrame(() => readerContext.correctPdfCrossPageSelection(x, y, start)); | |
| }, { passive: true }); | |
| document.addEventListener("pointercancel", () => { readerContext.pdfSelectionPointer = null; }, { passive: true }); | |
| } | |
| function bindSelectionAndPageLifecycle() { | |
| document.addEventListener("selectionchange", () => { | |
| if (readerContext.pdfSelectionBoundaryUpdate || | |
| !["pdf", "pdf-pages"].includes(readerContext.capability.mode)) return; | |
| const selection = document.getSelection(); | |
| if (!selection || selection.isCollapsed || !selection.rangeCount) return; | |
| const runs = [...readerContext.content.querySelectorAll(".reader-pdf-text-run")] | |
| .filter((run) => run.firstChild?.nodeType === Node.TEXT_NODE); | |
| if (!runs.length) return; | |
| const anchorRun = selection.anchorNode?.parentElement?.closest(".reader-pdf-text-run"); | |
| const focusRun = selection.focusNode?.parentElement?.closest(".reader-pdf-text-run"); | |
| if (!anchorRun && !focusRun) return; | |
| if (focusRun) return; | |
| // Separators (line ends, inter-block gaps) are selectable text nodes | |
| // directly inside the layer; a focus there is already valid text. | |
| const focusElement = selection.focusNode?.nodeType === Node.ELEMENT_NODE | |
| ? selection.focusNode : selection.focusNode?.parentElement; | |
| if (focusElement?.closest(".reader-pdf-text")) return; | |
| const beforeAnchor = selection.focusNode && selection.anchorNode && | |
| (selection.focusNode === selection.anchorNode | |
| ? selection.focusOffset < selection.anchorOffset | |
| : Boolean(selection.focusNode.compareDocumentPosition(selection.anchorNode) & Node.DOCUMENT_POSITION_FOLLOWING)); | |
| // Clamp to the run nearest the focus in document order. Choosing the | |
| // first/last run of the whole book jumps to a distant retained page | |
| // when the focus lands on a page whose text layer is not loaded yet. | |
| const focus = document.createRange(); | |
| focus.setStart(selection.focusNode, selection.focusOffset); | |
| let low = 0, high = runs.length; | |
| while (low < high) { | |
| const middle = (low + high) >> 1; | |
| if (focus.comparePoint(runs[middle].firstChild, 0) < 0) low = middle + 1; | |
| else high = middle; | |
| } | |
| const target = beforeAnchor ? runs[Math.min(low, runs.length - 1)] : runs[Math.max(low - 1, 0)]; | |
| const node = target.firstChild; | |
| readerContext.pdfSelectionBoundaryUpdate = true; | |
| try { | |
| selection.setBaseAndExtent(selection.anchorNode, selection.anchorOffset, node, | |
| beforeAnchor ? 0 : node.length); | |
| } finally { | |
| readerContext.pdfSelectionBoundaryUpdate = false; | |
| } | |
| }, { passive: true }); | |
| window.addEventListener("pagehide", (event) => { | |
| readerContext.saveProgress(event); | |
| if (!event.persisted) readerContext.disposeReader(); | |
| }); | |
| window.addEventListener("pageshow", (event) => { | |
| if (event.persisted && !readerContext.readerLifecycle.disposed) { | |
| readerContext.scheduleFoliateScrollSync(); | |
| readerContext.updateProgressTools(); | |
| } | |
| }); | |
| document.addEventListener("visibilitychange", () => { | |
| if (document.visibilityState === "hidden") readerContext.saveProgress(); | |
| if (document.hidden) readerContext.stopPdfManifestPrefetch(); | |
| else readerContext.queuePdfManifestPrefetch(); | |
| }); | |
| window.addEventListener("offline", readerContext.stopPdfManifestPrefetch); | |
| window.addEventListener("online", () => readerContext.queuePdfManifestPrefetch()); | |
| } | |
| function mountProgressControls() { | |
| readerContext.progressTools.className = "reader-progress-tools"; | |
| readerContext.progressTools.innerHTML = | |
| '<div class="reader-progress-row"><button class="reader-chapter-prev" type="button" aria-label="上一章" title="上一章"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m14 5-7 7 7 7"/></svg></button><input class="reader-progress-range" type="range" min="0" max="100" step="0.1" value="0" aria-label="阅读进度"><span class="reader-progress-percent">0.0%</span><button class="reader-progress-undo" type="button" aria-label="撤销进度调整" title="撤销进度调整"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7.2 8.3A6.5 6.5 0 1 1 6 16"/><path d="m7.2 8.3 3-.2"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/></svg></button><button class="reader-chapter-next" type="button" aria-label="下一章" title="下一章"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m10 5 7 7-7 7"/></svg></button></div>'; | |
| document.querySelector("#history-panel").appendChild(readerContext.progressTools); | |
| } | |
| function mountProgressSummary() { | |
| readerContext.readingProgressSummary.className = "reader-progress-summary"; | |
| readerContext.readingProgressSummary.innerHTML = | |
| '<strong>阅读状态</strong><span class="reader-progress-summary-value">0.0%</span><button class="reader-progress-bookmark text-action" type="button">在当前位置添加书签</button>'; | |
| readerContext.progressTools.prepend(readerContext.readingProgressSummary); | |
| readerContext.readingProgressSummary | |
| .querySelector(".reader-progress-bookmark") | |
| .addEventListener("click", (event) => readerContext.openBookmarkPopover(event.currentTarget)); | |
| readerContext.updateReadingProgressSummary(); | |
| readerContext.syncTocTab(); | |
| } | |
| function bindProgressActions() { | |
| const startSeeking = () => { | |
| if (readerContext.progressSeeking) return; | |
| readerContext.beginReaderNavigation(); | |
| readerContext.progressSeeking = true; | |
| readerContext.previousProgressState = readerContext.captureProgressState(); | |
| }; | |
| const finishSeeking = () => { | |
| if (!readerContext.progressSeeking) return; | |
| readerContext.progressSeeking = false; | |
| const generation = readerContext.readerRuntime.currentGeneration("navigation"); | |
| readerContext.flushEpubSeek().then(() => { | |
| if (readerContext.isReaderGenerationCurrent("navigation", generation)) readerContext.updateProgressTools(); | |
| }); | |
| }; | |
| readerContext.progressRange.addEventListener("pointerdown", startSeeking); | |
| const seekKeys = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End"]; | |
| readerContext.progressRange.addEventListener("keydown", (event) => { | |
| if (seekKeys.includes(event.key) && !event.defaultPrevented && !event.altKey && !event.ctrlKey && !event.metaKey) | |
| startSeeking(); | |
| }); | |
| readerContext.progressRange.addEventListener("keyup", (event) => { | |
| if (seekKeys.includes(event.key)) finishSeeking(); | |
| }); | |
| readerContext.progressRange.addEventListener("blur", finishSeeking); | |
| readerContext.progressRange.addEventListener("input", () => { | |
| readerContext.progressPercent.textContent = `${Number(readerContext.progressRange.value).toFixed(1)}%`; | |
| readerContext.progressRange.setAttribute("aria-valuetext", readerContext.progressPercent.textContent); | |
| readerContext.seekProgress(readerContext.progressRange.value, true); | |
| }); | |
| for (const type of ["pointerup", "pointercancel"]) | |
| readerContext.progressRange.addEventListener(type, finishSeeking); | |
| readerContext.progressUndo.addEventListener("click", () => { | |
| const restoreFocus = document.activeElement === readerContext.progressUndo; | |
| const state = readerContext.previousProgressState; | |
| readerContext.previousProgressState = null; | |
| readerContext.progressSeeking = false; | |
| readerContext.restoreProgressState(state); | |
| readerContext.updateProgressTools(); | |
| if (restoreFocus) readerContext.progressRange.focus({ preventScroll: true }); | |
| }); | |
| readerContext.chapterPrev.addEventListener("click", () => | |
| readerContext.activateChapter(readerContext.adjacentTocIndex(-1)) | |
| ); | |
| readerContext.chapterNext.addEventListener("click", () => | |
| readerContext.activateChapter(readerContext.adjacentTocIndex(1)) | |
| ); | |
| } | |
| function ownDomSearchController() { | |
| readerContext.trackReaderResource(readerContext.domSearch.dispose); | |
| } | |
| function ownSearchViewController() { | |
| readerContext.trackReaderResource(readerContext.fullSearchController.dispose); | |
| } | |
| function ownPdfSearchStorageAndStart() { | |
| readerContext.trackReaderResource(() => { | |
| readerContext.pdfPersistentTextStore?.dispose(); | |
| readerContext.pdfPersistentTextStore = readerContext.pdfPersistentTextOpening = readerContext.pdfPersistentTextVersion = null; | |
| readerContext.pdfSearchTextCache.clear(); | |
| readerContext.pdfSearchTextCacheBytes = 0; | |
| }); | |
| readerContext.registerReaderFormatAdapters(); | |
| readerContext.syncCapabilityControls(); | |
| readerContext.updateProgressTools(); | |
| readerContext.start().catch((error) => { | |
| if (readerContext.readerAbortController.signal.aborted || error?.name === "AbortError") return; | |
| console.error(error); | |
| readerContext.fail("阅读文件解析失败,请刷新后重试,或下载原文件。", readerContext.classifyReaderError(error)); | |
| }); | |
| } | |
| return { | |
| trackStartupOwners, | |
| trackWorkCancellation, | |
| trackEarlySourceResolution, | |
| publishDiagnosticsAndInstallCompatibility, | |
| readSelectedSourceSession, | |
| normalizeConvertedPdfMode, | |
| applySourceSessionAndCachedMetadata, | |
| publishSourceAndSubscribeToStore, | |
| initializePanelAccessibility, | |
| mountInitialLoadingFeedback, | |
| bindDownloadAction, | |
| initializeDocumentTitle, | |
| restoreReturnNavigation, | |
| validateReturnHistory, | |
| ownChapterSearchClient, | |
| ownIndependentPdfResources, | |
| ownPdfBookSearchClient, | |
| ownPdfPrefetchAndQueues, | |
| initializeGenerations, | |
| labelToolbarControls, | |
| applyMetadataAndWatchSanitizedContent, | |
| initializeBookmarkEditor, | |
| initializeBookmarkSearchControls, | |
| mountBookmarkHeaderActions, | |
| mountMediaPanel, | |
| restoreAndBindPageInversion, | |
| restoreReaderThemeAndBindReturn, | |
| bindZoomAndPageControls, | |
| bindReaderUiHistory, | |
| bindPanelBookmarkAndLifecycleControls, | |
| bindPdfPointerSelection, | |
| bindSelectionAndPageLifecycle, | |
| mountProgressControls, | |
| mountProgressSummary, | |
| bindProgressActions, | |
| ownDomSearchController, | |
| ownSearchViewController, | |
| ownPdfSearchStorageAndStart | |
| }; | |
| } | |