Download static/reader/ui/reader-progress.mjs from VoiceOfML/Search: direct link, hf CLI and curl.
- Browser
- Download file 21.5 kB
-
https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader/ui/reader-progress.mjs
- Command line
-
hf download hf://spaces/VoiceOfML/Search/static/reader/ui/reader-progress.mjs
-
curl -L -o reader-progress.mjs https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader/ui/reader-progress.mjs
21.5 kB
| export function createReaderProgress(readerContext) { | |
| function scheduleSave() { | |
| if (readerContext.readerLifecycle.disposed || readerContext.readerAbortController.signal.aborted) return; | |
| const now = performance.now(); | |
| if (!readerContext.saveDeadline) readerContext.saveDeadline = now + 2000; | |
| readerContext.readerRuntime.cancel(readerContext.saveTimer); | |
| readerContext.saveTimer = readerContext.readerRuntime.schedule( | |
| () => { | |
| readerContext.saveTimer = 0; | |
| readerContext.saveDeadline = 0; | |
| readerContext.saveProgress(); | |
| }, | |
| Math.max(0, Math.min(500, readerContext.saveDeadline - now)) | |
| ); | |
| } | |
| async function saveProgress(event) { | |
| if ( | |
| readerContext.readerLifecycle.disposed || | |
| !readerContext.documentState.restorationReady || | |
| !readerContext.validSource(readerContext.sourceUrl) || | |
| readerContext.historySuppressed || | |
| readerContext.mediaElement?._readerSourceRetry | |
| ) | |
| return; | |
| if (event?.type !== "pagehide" && document.visibilityState !== "hidden") | |
| readerContext.syncCurrentPageFromMarker(); | |
| const shell = readerContext.documentState.pageCount | |
| ? readerContext.content.querySelector( | |
| `.reader-page[data-page="${readerContext.documentState.page}"]` | |
| ) | |
| : null; | |
| const pageOffset = shell ? Math.max(0, readerContext.viewport.scrollTop - readerContext.readerPageScrollTop(shell)) : 0; | |
| const htmlScrollTop = readerContext.htmlFrame && readerContext.htmlFrame.contentWindow ? readerContext.htmlFrame.contentWindow.scrollY : 0; | |
| const readerUrl = readerContext.currentReaderUrl(); | |
| const foliatePosition = readerContext.captureFoliateBookmarkPosition() || readerContext.captureChapterPosition(); | |
| const progress = { | |
| url: readerContext.sourceUrl, | |
| title: readerContext.documentState.title, | |
| extension: readerContext.extension, | |
| readerUrl: readerUrl.href, | |
| page: readerContext.documentState.page, | |
| pageCount: readerContext.documentState.pageCount, | |
| pageOffset, | |
| mediaTime: | |
| readerContext.mediaElement && Number.isFinite(readerContext.mediaElement.currentTime) ? readerContext.mediaElement.currentTime : 0, | |
| scrollTop: readerContext.viewport.scrollTop, | |
| htmlScrollTop, | |
| zoom: Math.round(readerContext.documentState.zoom * 100), | |
| ...(foliatePosition || {}) | |
| }; | |
| const signature = JSON.stringify(progress); | |
| if (signature === readerContext.lastSavedProgress) return readerContext.progressSaveChain; | |
| readerContext.lastSavedProgress = signature; | |
| readerContext.progressSaveChain = readerContext.progressSaveChain | |
| .catch(() => {}) | |
| .then(() => { | |
| if (!readerContext.historySuppressed) | |
| return VoiceOfMLReaderStore.put({ ...progress, lastReadAt: Date.now() }); | |
| }) | |
| .catch((error) => { | |
| if (readerContext.lastSavedProgress === signature) readerContext.lastSavedProgress = ""; | |
| console.warn("Reader progress was not saved", error); | |
| }); | |
| return readerContext.progressSaveChain; | |
| } | |
| function placeReadingProgress() { | |
| const panel = document.querySelector("#history-panel"), | |
| tocPanel = document.querySelector("#toc-panel"), | |
| tocList = tocPanel.querySelector(".panel-list"), | |
| noToc = readerContext.loadingStatus.hidden && !readerContext.navigationState.tocEntries.length && !readerContext.mediaElement; | |
| readerContext.progressTools.classList.toggle("reader-no-toc", noToc); | |
| tocPanel.querySelector(".panel-view-header strong").textContent = noToc ? "阅读状态" : "目录"; | |
| if ( | |
| noToc && | |
| (readerContext.progressTools.parentElement !== tocPanel || readerContext.progressTools.nextElementSibling !== tocList) | |
| ) | |
| tocPanel.insertBefore(readerContext.progressTools, tocList); | |
| else if (!noToc && panel.lastElementChild !== readerContext.progressTools) panel.appendChild(readerContext.progressTools); | |
| } | |
| function currentReaderUrl() { | |
| const url = new URL(location.href); | |
| for (const key of ["return", "nav", "bookmark", "bookmark_source"]) url.searchParams.delete(key); | |
| return url; | |
| } | |
| async function loadReaderRestoration() { | |
| const currentEntry = (entry) => { | |
| if ( | |
| readerContext.capability.mode === "epub-chapters" && | |
| /-epub-chapters-v5\//.test(readerContext.chapterManifestUrl || readerContext.sourceUrl) && | |
| entry?.chapterManifest !== (readerContext.chapterManifestUrl || readerContext.sourceUrl) | |
| ) return null; | |
| return entry; | |
| }; | |
| try { | |
| const bookmarkId = readerContext.params.get("bookmark"), | |
| bookmarkSource = readerContext.params.get("bookmark_source"); | |
| if (bookmarkId && bookmarkSource === readerContext.sourceUrl) { | |
| const entries = await VoiceOfMLReaderStore.listBookmarks(readerContext.sourceUrl); | |
| const bookmark = entries.find((entry) => entry.id === bookmarkId && entry.url === readerContext.sourceUrl); | |
| if (bookmark) return currentEntry(bookmark); | |
| } | |
| return currentEntry(await VoiceOfMLReaderStore.get(readerContext.sourceUrl)); | |
| } catch (_) { | |
| readerContext.restorationFailed = true; | |
| return null; | |
| } | |
| } | |
| async function getInitialReaderRestoration() { | |
| if (!readerContext.readerRestorationPromise) return null; | |
| if (!readerContext.readerRestorationLoaded) { | |
| readerContext.readerRestorationEntry = await readerContext.awaitReader(Promise.race([ | |
| readerContext.readerRestorationPromise, | |
| readerContext.waitForReader(150).then(() => null) | |
| ])); | |
| readerContext.readerRestorationLoaded = true; | |
| } | |
| return readerContext.readerRestorationEntry; | |
| } | |
| function initialReaderPage(total) { | |
| const page = Number(readerContext.readerRestorationEntry?.page); | |
| return Number.isInteger(page) ? Math.max(1, Math.min(total, page)) : 1; | |
| } | |
| function readerProgressPercent() { | |
| if (readerContext.mediaElement) | |
| return readerContext.mediaElement.duration | |
| ? Math.round((readerContext.mediaElement.currentTime / readerContext.mediaElement.duration) * 1000) / 10 | |
| : 0; | |
| if (readerContext.foliateContinuous) { | |
| const position = readerContext.captureFoliateBookmarkPosition(), | |
| count = readerContext.epubBook?.sections?.filter((section) => section.linear !== "no").length || 0, | |
| article = | |
| position && | |
| document.querySelector( | |
| `.foliate-continuous article[data-section="${position.foliateSection}"]` | |
| ), | |
| fraction = article | |
| ? Math.max( | |
| 0, | |
| Math.min( | |
| 1, | |
| position.foliateOffset / Math.max(1, article.getBoundingClientRect().height) | |
| ) | |
| ) | |
| : 0; | |
| return count && position | |
| ? Math.round(((position.foliateSection + fraction) / count) * 1000) / 10 | |
| : 0; | |
| } | |
| if (readerContext.htmlFrame && readerContext.htmlFrame.contentDocument) { | |
| const doc = readerContext.htmlFrame.contentDocument.documentElement, | |
| win = readerContext.htmlFrame.contentWindow; | |
| return ( | |
| Math.round( | |
| Math.max(0, Math.min(1, win.scrollY / Math.max(1, doc.scrollHeight - win.innerHeight))) * | |
| 1000 | |
| ) / 10 | |
| ); | |
| } | |
| return ( | |
| Math.round( | |
| Math.max( | |
| 0, | |
| Math.min(1, readerContext.viewport.scrollTop / Math.max(1, readerContext.viewport.scrollHeight - readerContext.viewport.clientHeight)) | |
| ) * 1000 | |
| ) / 10 | |
| ); | |
| } | |
| function pageAtMarker() { | |
| const y = readerContext.bookmarkRibbon.getBoundingClientRect().bottom, | |
| pages = [...readerContext.content.querySelectorAll(".reader-page")]; | |
| if (!pages.length) return null; | |
| const end = readerContext.viewport.scrollHeight - readerContext.viewport.clientHeight; | |
| if (["pdf", "pdf-pages"].includes(readerContext.capability.mode) && end > 2 && readerContext.viewport.scrollTop > 0 && | |
| end - readerContext.viewport.scrollTop <= 2) { | |
| const last = pages[pages.length - 1], rect = last.getBoundingClientRect(), view = readerContext.viewport.getBoundingClientRect(); | |
| if (rect.bottom > view.top && rect.top < view.bottom) return last; | |
| } | |
| let low = 0, high = pages.length; | |
| while (low < high) { | |
| const middle = (low + high) >> 1; | |
| if (pages[middle].getBoundingClientRect().bottom > y) high = middle; | |
| else low = middle + 1; | |
| } | |
| return pages[Math.min(low, pages.length - 1)]; | |
| } | |
| function syncCurrentPageFromMarker() { | |
| if (performance.now() < readerContext.pageNavigationLockUntil) return; | |
| const page = readerContext.pageAtMarker(); | |
| if (!page) return; | |
| const next = Number(page.dataset.page); | |
| if (!next || next === readerContext.documentState.page) return; | |
| readerContext.updateDocumentState({ page: next }); | |
| if (document.activeElement !== readerContext.pageInput) readerContext.pageInput.value = String(next); | |
| readerContext.updateTocCurrentMark(); | |
| readerContext.updateProgressTools(); | |
| } | |
| function scheduleMarkerSync() { | |
| if (readerContext.markerFrame) return; | |
| readerContext.markerFrame = requestAnimationFrame(() => { | |
| readerContext.markerFrame = 0; | |
| if (readerContext.readerAbortController.signal.aborted) return; | |
| readerContext.syncCurrentPageFromMarker(); | |
| readerContext.refreshNearbyPdfPages(); | |
| readerContext.syncHeadingLocation(); | |
| readerContext.updateProgressTools(); | |
| readerContext.scheduleSave(); | |
| }); | |
| } | |
| async function restoreFormat(mode, entry, generation = readerContext.beginReaderNavigation()) { | |
| try { | |
| if (!entry || !readerContext.isReaderGenerationCurrent("navigation", generation)) return false; | |
| if (mode === "epub-chapters") { | |
| if (Number.isInteger(entry.chapterIndex)) { | |
| if (!(await readerContext.restoreChapterPosition(entry, generation))) return false; | |
| } else if (!(await readerContext.restoreProgressState(entry, generation))) return false; | |
| } else if (mode === "foliate" && Number.isInteger(entry.foliateSection)) { | |
| if (!(await readerContext.restoreFoliateBookmarkPosition(entry, generation))) return false; | |
| } else if (readerContext.documentState.pageCount && entry.page) { | |
| if ( | |
| !(await readerContext.goToPage(entry.page, generation)) || | |
| !readerContext.isReaderGenerationCurrent("navigation", generation) | |
| ) | |
| return false; | |
| const shell = readerContext.content.querySelector( | |
| `.reader-page[data-page="${readerContext.documentState.page}"]` | |
| ); | |
| if (shell && Number.isFinite(entry.pageOffset)) | |
| readerContext.viewport.scrollTop = readerContext.readerPageScrollTop(shell) + entry.pageOffset; | |
| readerContext.updateProgressTools(); | |
| readerContext.scheduleSave(); | |
| } else if (!(await readerContext.restoreProgressState(entry, generation))) return false; | |
| if (!readerContext.isReaderGenerationCurrent("navigation", generation)) return false; | |
| readerContext.consumeBookmarkHandoff(entry); | |
| return true; | |
| } catch (error) { | |
| readerContext.reportNavigationError(error, generation); | |
| return false; | |
| } | |
| } | |
| async function restoreInitialPosition(entry, generation) { | |
| if (!entry || !readerContext.isReaderGenerationCurrent("navigation", generation)) return; | |
| const restored = await readerContext.formatAdapters.active.restore(entry, generation); | |
| if (!restored && readerContext.isReaderGenerationCurrent("navigation", generation)) | |
| throw Object.assign(new Error("阅读位置恢复失败,原进度已保留,请重试加载。"), { | |
| code: "READER_RESTORE" | |
| }); | |
| } | |
| async function applyInitialRestoration(entry, generation) { | |
| readerContext.updateDocumentState({ restoredEntry: entry }); | |
| // Startup already applied the current zoom. Reapplying an identical saved | |
| // value needlessly invalidates PDF canvases and lays out every format again. | |
| if (entry?.zoom && !readerContext.zoomChangedByUser && readerContext.isReaderGenerationCurrent("navigation", generation) && | |
| VoiceOfMLReader.clampNumber(entry.zoom, 25, 400, 100) / 100 !== readerContext.documentState.zoom) | |
| readerContext.setZoom(entry.zoom, false); | |
| await readerContext.restoreInitialPosition(entry, generation); | |
| } | |
| function updateReadingProgressSummary() { | |
| const noToc = !readerContext.navigationState.tocEntries.length && !readerContext.mediaElement; | |
| readerContext.readingProgressSummary.hidden = !noToc; | |
| if (noToc) | |
| readerContext.readingProgressSummary.querySelector(".reader-progress-summary-value").textContent = | |
| `${readerContext.readerProgressPercent().toFixed(1)}%`; | |
| } | |
| function updateProgressTools() { | |
| if (readerContext.progressSeeking) return; | |
| const percent = readerContext.readerProgressPercent(); | |
| readerContext.progressRange.value = String(percent); | |
| readerContext.progressPercent.textContent = `${percent.toFixed(1)}%`; | |
| if (readerContext.progressRange.getAttribute("aria-valuetext") !== readerContext.progressPercent.textContent) | |
| readerContext.progressRange.setAttribute("aria-valuetext", readerContext.progressPercent.textContent); | |
| const hasChapters = readerContext.navigationState.tocEntries.some((entry) => !entry.group), | |
| hasPreviousChapter = readerContext.adjacentTocIndex(-1) >= 0, | |
| hasNextChapter = readerContext.adjacentTocIndex(1) >= 0; | |
| readerContext.chapterPrev.hidden = !hasChapters; | |
| readerContext.chapterNext.hidden = !hasChapters; | |
| readerContext.chapterPrev.disabled = !hasPreviousChapter; | |
| readerContext.chapterNext.disabled = !hasNextChapter; | |
| readerContext.progressUndo.hidden = !readerContext.previousProgressState; | |
| readerContext.updateReadingProgressSummary(); | |
| readerContext.placeReadingProgress(); | |
| } | |
| function captureProgressState() { | |
| return readerContext.foliateContinuous | |
| ? readerContext.captureFoliateBookmarkPosition() | |
| : readerContext.capability.mode === "epub-chapters" | |
| ? readerContext.captureChapterPosition() | |
| : readerContext.mediaElement | |
| ? { mediaTime: readerContext.mediaElement.currentTime } | |
| : readerContext.htmlFrame && readerContext.htmlFrame.contentWindow | |
| ? { htmlScrollTop: readerContext.htmlFrame.contentWindow.scrollY } | |
| : { scrollTop: readerContext.viewport.scrollTop }; | |
| } | |
| async function restoreProgressState(state, generation = readerContext.beginReaderNavigation()) { | |
| if (!state || !readerContext.isReaderGenerationCurrent("navigation", generation)) return false; | |
| if (readerContext.foliateContinuous && Number.isInteger(state.foliateSection)) | |
| return readerContext.restoreFoliateBookmarkPosition(state, generation); | |
| if (readerContext.capability.mode === "epub-chapters" && Number.isInteger(state.chapterIndex)) | |
| return readerContext.restoreChapterPosition(state, generation); | |
| try { | |
| if (readerContext.mediaElement && Number.isFinite(state.mediaTime)) { | |
| const media = readerContext.mediaElement; | |
| if (!media.readyState) | |
| await new Promise((resolve, reject) => { | |
| const finish = (error) => { | |
| media.removeEventListener("loadedmetadata", loaded); | |
| media.removeEventListener("error", failed); | |
| untrack(); | |
| if (error) reject(error); | |
| else resolve(); | |
| }; | |
| const loaded = () => finish(), | |
| failed = () => { | |
| if (!media._readerSourceRetry) finish(new Error("Media metadata unavailable")); | |
| }; | |
| const untrack = readerContext.trackReaderResource(() => finish(readerContext.readerAbortError())); | |
| media.addEventListener("loadedmetadata", loaded, { once: true }); | |
| media.addEventListener("error", failed); | |
| }); | |
| if (!readerContext.isReaderGenerationCurrent("navigation", generation)) return false; | |
| media.currentTime = state.mediaTime; | |
| } else if (readerContext.htmlFrame && readerContext.htmlFrame.contentWindow && Number.isFinite(state.htmlScrollTop)) | |
| readerContext.htmlFrame.contentWindow.scrollTo(0, state.htmlScrollTop); | |
| else if (Number.isFinite(state.scrollTop)) { | |
| if (readerContext.capability.mode === "text") { | |
| let measured = 0, batch = 0, started = performance.now(); | |
| for (const block of readerContext.content.querySelectorAll(".reader-text-block")) { | |
| if (!readerContext.isReaderGenerationCurrent("navigation", generation)) return false; | |
| block.style.contentVisibility = "visible"; | |
| const height = block.getBoundingClientRect().height; | |
| block.style.containIntrinsicBlockSize = `auto ${height}px`; | |
| block.style.contentVisibility = "auto"; | |
| measured += height; | |
| if (measured > state.scrollTop + readerContext.viewport.clientHeight) break; | |
| if (++batch >= 8 || performance.now() - started >= 8) { | |
| await readerContext.waitForReader(); | |
| batch = 0; | |
| started = performance.now(); | |
| } | |
| } | |
| } | |
| if (!readerContext.isReaderGenerationCurrent("navigation", generation)) return false; | |
| readerContext.viewport.scrollTop = state.scrollTop; | |
| } | |
| readerContext.updateProgressTools(); | |
| readerContext.scheduleSave(); | |
| return true; | |
| } catch (error) { | |
| readerContext.reportNavigationError(error, generation); | |
| return false; | |
| } | |
| } | |
| function flushEpubSeek() { | |
| if (readerContext.epubSeekFrame) { | |
| cancelAnimationFrame(readerContext.epubSeekFrame); | |
| readerContext.epubSeekFrame = 0; | |
| readerContext.epubSeekPromise = readerContext.seekFoliateProgress(readerContext.epubSeekTarget.percent, readerContext.epubSeekTarget.generation); | |
| } | |
| return readerContext.epubSeekPromise; | |
| } | |
| function seekProgress(value, preserve = false) { | |
| if (!preserve || !readerContext.previousProgressState) readerContext.previousProgressState = readerContext.captureProgressState(); | |
| const generation = readerContext.beginReaderNavigation(), | |
| percent = Math.max(0, Math.min(100, Number(value))); | |
| if (!readerContext.isReaderGenerationCurrent("navigation", generation)) return; | |
| if (readerContext.mediaElement && readerContext.mediaElement.duration) | |
| readerContext.mediaElement.currentTime = (readerContext.mediaElement.duration * percent) / 100; | |
| else if (readerContext.foliateContinuous) { | |
| readerContext.epubSeekTarget = { percent, generation }; | |
| readerContext.epubSeekFrame = requestAnimationFrame(() => { | |
| readerContext.epubSeekFrame = 0; | |
| readerContext.epubSeekPromise = readerContext.seekFoliateProgress(percent, generation); | |
| }); | |
| } else if (readerContext.capability.mode === "epub-chapters" && readerContext.chapterManifestLoader?.count) { | |
| const position = Math.min(0.999999, percent / 100) * readerContext.chapterManifestLoader.count; | |
| readerContext.epubSeekPromise = (async () => { | |
| const index = Math.floor(position) + 1; | |
| const article = await readerContext.chapterManifestLoader(index); | |
| if (!article?.isConnected || !readerContext.isReaderGenerationCurrent("navigation", generation)) return false; | |
| return readerContext.restoreChapterPosition({chapterIndex:index,chapterManifest:readerContext.chapterManifestUrl || readerContext.sourceUrl, | |
| chapterOffset:article.getBoundingClientRect().height * (position - Math.floor(position))}, generation); | |
| })().catch(error => { readerContext.reportNavigationError(error, generation); return false; }); | |
| } else if (readerContext.htmlFrame && readerContext.htmlFrame.contentWindow) | |
| readerContext.htmlFrame.contentWindow.scrollTo( | |
| 0, | |
| (Math.max( | |
| 0, | |
| readerContext.htmlFrame.contentDocument.documentElement.scrollHeight - readerContext.htmlFrame.contentWindow.innerHeight | |
| ) * | |
| percent) / | |
| 100 | |
| ); | |
| else | |
| readerContext.viewport.scrollTop = | |
| (Math.max(0, readerContext.viewport.scrollHeight - readerContext.viewport.clientHeight) * percent) / 100; | |
| readerContext.updateProgressTools(); | |
| readerContext.scheduleSave(); | |
| } | |
| async function activateChapter(index) { | |
| if ( | |
| index < 0 || | |
| index >= readerContext.navigationState.tocEntries.length || | |
| index === readerContext.navigationState.currentChapterIndex | |
| ) | |
| return; | |
| try { | |
| await readerContext.navigateTocEntry(index); | |
| } finally { | |
| readerContext.updateProgressTools(); | |
| } | |
| } | |
| function adjacentTocIndex(direction) { | |
| for (let index = readerContext.navigationState.currentChapterIndex + direction; | |
| index >= 0 && index < readerContext.navigationState.tocEntries.length; index += direction) { | |
| if (!readerContext.navigationState.tocEntries[index].group) return index; | |
| } | |
| return -1; | |
| } | |
| return { | |
| scheduleSave, | |
| saveProgress, | |
| placeReadingProgress, | |
| currentReaderUrl, | |
| loadReaderRestoration, | |
| getInitialReaderRestoration, | |
| initialReaderPage, | |
| readerProgressPercent, | |
| pageAtMarker, | |
| syncCurrentPageFromMarker, | |
| scheduleMarkerSync, | |
| restoreFormat, | |
| restoreInitialPosition, | |
| applyInitialRestoration, | |
| updateReadingProgressSummary, | |
| updateProgressTools, | |
| captureProgressState, | |
| restoreProgressState, | |
| flushEpubSeek, | |
| seekProgress, | |
| activateChapter, | |
| adjacentTocIndex | |
| }; | |
| } | |