Search / static /reader /formats /reader-format-controller.mjs
vomebook's picture
fix: remaining review items (backend proxies/OCR/order, Reader framing/storage/early Back, SW fallback, search UI)
b01d918 verified
Raw History Blame Contribute Delete
14.5 kB
export function createReaderFormatController(readerContext) {
function disposeFormatResources(mode) {
if (mode === "epub-chapters") {
readerContext.chapterManifestCleanup?.();
readerContext.chapterManifestCleanup = null;
}
if (mode === "swf") {
readerContext.swfPlayer?.remove();
readerContext.swfPlayer = null;
}
if (["pdf", "pdf-pages", "image-pages"].includes(mode)) {
readerContext.pdfDocument = null;
readerContext.pdfHybridUrl = "";
readerContext.pdfHybridDocumentPromise = null;
readerContext.pdfHybridSearchUnavailable = false;
readerContext.pdfPageManifest = null;
readerContext.pdfOcrManifest = null;
readerContext.pdfOcrManifestPromise = null;
readerContext.pdfV3Repository?.dispose(); readerContext.pdfTextLayerRepository?.dispose();
for (const shell of readerContext.pdfManifestShells) shell?._v3PreviewRelease?.();
readerContext.pdfV3Repository = readerContext.pdfTextLayerRepository = null;
readerContext.pdfV3Manifest = readerContext.pdfV3Map = null;
readerContext.pdfTextContentCache.clear();
readerContext.pdfTextContentPromises.clear();
readerContext.pdfTextContentCacheBytes = 0;
readerContext.pdfOcrPageCache.clear();
readerContext.pdfOcrPageCacheBytes = 0;
readerContext.clearPdfManifestPrefetches();
readerContext.pdfBookSearchClient?.dispose();
readerContext.pdfBookSearchClient = null;
readerContext.pdfShellWindow?.dispose();
readerContext.pdfFirstPagePreload?.removeAttribute("src");
readerContext.pdfFirstPagePreload = null;
for (const canvas of readerContext.content.querySelectorAll(".reader-page canvas")) {
canvas.width = 0;
canvas.height = 0;
}
for (const image of readerContext.content.querySelectorAll(".reader-page img")) image.removeAttribute("src");
}
if (mode === "foliate") {
readerContext.foliateSectionObserver?.disconnect();
readerContext.foliateSectionObserver = null;
readerContext.foliateSectionVirtualizer?.dispose();
readerContext.foliateChapterRepository?.dispose();
readerContext.foliateSectionVirtualizer = null;
readerContext.foliateChapterRepository = null;
readerContext.foliateSectionLoader = null;
readerContext.foliateSectionSettler = null;
try {
const closing = readerContext.epubRendition?.close?.();
closing?.catch?.(() => {});
} catch (_) {}
readerContext.epubRendition = null;
readerContext.epubBook = null;
}
if (mode === "html") {
readerContext.htmlFrame?.remove();
readerContext.htmlFrame = null;
readerContext.content.classList.remove("reader-content--spreadsheet");
}
if (["audio", "video"].includes(mode) && readerContext.mediaElement) {
readerContext.mediaElement.pause();
readerContext.mediaElement.removeAttribute("src");
readerContext.mediaElement.load();
readerContext.mediaElement = null;
}
}
function registerReaderFormatAdapters() {
const formats = {
"pdf-pages": [
() => {
return readerContext.fetchReaderResponse();
},
readerContext.renderPdfPages
],
"image-pages": [
() => readerContext.fetchReaderResponse(),
readerContext.renderImagePages
],
pdf: [readerContext.loadPdfDocument, readerContext.renderPdf],
image: [readerContext.loadImageDocument, readerContext.renderImageDocument],
text: [readerContext.loadTextDocument, readerContext.renderTextDocument],
markdown: [readerContext.loadMarkdownDocument, readerContext.renderMarkdownDocument],
html: [readerContext.loadHtmlDocument, readerContext.renderHtml],
"epub-chapters": [readerContext.loadChapterManifestDocument, readerContext.renderChapterManifest],
audio: [readerContext.loadMediaDocument, () => readerContext.renderMedia("audio")],
video: [readerContext.loadMediaDocument, () => readerContext.renderMedia("video")],
swf: [readerContext.loadSwfDocument, readerContext.renderSwf],
foliate: [readerContext.loadMediaDocument, readerContext.renderFoliate]
};
for (const [mode, [open, render]] of Object.entries(formats))
readerContext.formatAdapters.register(mode, {
open,
render,
restore: (entry, generation) => readerContext.restoreFormat(mode, entry, generation),
dispose: () => readerContext.disposeFormatResources(mode)
});
}
async function resolveReaderSource() {
if (!readerContext.readerId || readerContext.sourceUrl) return;
readerContext.setReaderStage("resolve");
let resolved = readerContext.cachedReaderData;
if (!resolved?.url) {
try {
resolved = await readerContext.resolveReaderId(readerContext.readerId);
} catch (error) {
readerContext.assertReaderActive();
if (!readerContext.localReaderData?.url) throw error;
resolved = readerContext.localReaderData;
}
}
readerContext.assertReaderActive();
readerContext.sourceUrl = readerContext.normalizeSourceUrl(resolved.url || "");
readerContext.contentUrl = readerContext.readerContentUrl(readerContext.sourceUrl);
readerContext.downloadUrl = resolved.download || readerContext.sourceUrl;
readerContext.chapterManifestUrl = resolved.chapter_manifest || "";
readerContext.fallbackUrl = resolved.fallback || "";
if (resolved.extension) readerContext.extension = String(resolved.extension).toLowerCase();
if (
readerContext.extension === "pdf" &&
readerContext.sourceUrl.includes("/api/reader-bucket-resource?") &&
new URL(readerContext.sourceUrl).searchParams.get("path")?.endsWith("page-manifest.json")
)
readerContext.extension = "pdf-pages";
readerContext.capability = readerContext.readerRuntime.negotiate(VoiceOfMLReader.capability(readerContext.extension));
if (readerContext.capability.mode !== "pdf" && !readerContext.pdfPrimarySource()) readerContext.releasePdfWorker?.();
readerContext.readerRuntime.update("source", {
url: readerContext.sourceUrl,
contentUrl: readerContext.contentUrl,
downloadUrl: readerContext.downloadUrl,
extension: readerContext.extension,
metadata: resolved
});
readerContext.applyReaderMetadata(resolved);
}
async function start() {
const generation = readerContext.readerRuntime.currentGeneration("navigation");
// A PDF link already tells us which engine is needed. Start its import while
// an ID-only Reader link is resolving its source URL. The promise is reused
// by loadPdfDocument, so this does not create a second module request.
if (readerContext.extension === "pdf" || /\.pdf(?:$|[?#])/i.test(readerContext.sourceUrl)) readerContext.preloadPdfEngine().catch(() => {});
readerContext.readerSourcePromise = readerContext.measurePdf(readerContext.resolveReaderSource(), "resolve");
await readerContext.readerSourcePromise;
readerContext.assertReaderActive();
if (readerContext.readerId && /\/calibre-chm-epub-[^/]+\/document\.epub(?:$|\?)/i.test(readerContext.sourceUrl)) {
readerContext.extension = "epub";
readerContext.readerRuntime.update("source", { extension: readerContext.extension });
readerContext.capability = readerContext.readerRuntime.negotiate(VoiceOfMLReader.capability(readerContext.extension));
readerContext.content.dataset.mode = readerContext.capability.mode;
document.querySelector(".page-controls").hidden = true;
}
readerContext.syncCapabilityControls();
readerContext.downloadButton.href = readerContext.readerDownloadUrl(readerContext.documentState.title, readerContext.downloadUrl);
if (
(!readerContext.validSource(readerContext.sourceUrl) && !readerContext.validSource(readerContext.chapterManifestUrl) && !readerContext.readerId) ||
readerContext.capability.readerMode === VoiceOfMLReader.ReaderMode.UNSUPPORTED
)
return readerContext.fail("此文件暂不支持在线阅读,请下载原文件。", "READER_UNSUPPORTED");
readerContext.formatAdapters.activate(readerContext.capability.mode);
readerContext.applyReaderMetadata(readerContext.resolvedReaderData);
if (readerContext.validOcr(readerContext.ocrUrl)) {
const ocr = document.querySelector("#ocr") || document.createElement("a");
ocr.id = "ocr";
ocr.className = "text-button";
ocr.target = "_blank";
ocr.rel = "noopener noreferrer";
ocr.textContent = "OCR";
if (!ocr.parentElement) document.querySelector(".reader-actions")?.prepend(ocr);
ocr.href = readerContext.ocrUrl;
ocr.hidden = false;
}
try {
if (readerContext.capability.mode === "foliate") {
readerContext.setReaderStage("foliate");
readerContext.foliateContinuous = true;
const restorationPromise = readerContext.loadReaderRestoration();
readerContext.readerRestorationPromise = restorationPromise;
await readerContext.awaitReader(readerContext.renderFoliate());
readerContext.assertReaderActive();
const restored = await readerContext.awaitReader(restorationPromise);
await readerContext.applyInitialRestoration(restored, generation);
readerContext.initialReaderTargetSection = null;
readerContext.assertReaderActive();
if (!readerContext.setReaderPhase("ready")) return;
readerContext.updateDocumentState({ restorationReady: !readerContext.restorationFailed });
readerContext.scheduleSave();
return;
}
readerContext.setReaderStage("prepare");
const restorationPromise = readerContext.loadReaderRestoration();
readerContext.readerRestorationPromise = restorationPromise;
const prepareResources = new Set(readerContext.readerResources);
try {
const prepared = await readerContext.awaitReader(readerContext.formatAdapters.active.open());
readerContext.assertReaderActive();
await readerContext.formatAdapters.active.render(prepared);
} catch (error) {
readerContext.assertReaderActive();
const originalExtension = String(readerContext.resolvedReaderData?.original_extension || "").toLowerCase();
if (readerContext.capability.mode !== "image-pages" || !readerContext.validSource(readerContext.downloadUrl) ||
!["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(originalExtension)) throw error;
for (const cleanup of [...readerContext.readerResources]) {
if (prepareResources.has(cleanup)) continue;
cleanup();
readerContext.readerResources.delete(cleanup);
}
readerContext.formatAdapters.dispose();
readerContext.content.replaceChildren();
readerContext.extension = originalExtension;
readerContext.capability = readerContext.readerRuntime.negotiate(VoiceOfMLReader.capability(readerContext.extension));
readerContext.readerRuntime.update("source", { extension: readerContext.extension });
readerContext.updateDocumentState({ page: 1, pageCount: 0 });
readerContext.formatAdapters.activate(readerContext.capability.mode);
readerContext.syncCapabilityControls();
await readerContext.renderImageDocument(await readerContext.loadImageDocument(readerContext.downloadUrl));
}
readerContext.assertReaderActive();
readerContext.markReaderContentReady();
const restored = await readerContext.awaitReader(restorationPromise);
await readerContext.applyInitialRestoration(restored, generation);
readerContext.assertReaderActive();
if (!readerContext.setReaderPhase("ready")) return;
readerContext.updateDocumentState({ restorationReady: !readerContext.restorationFailed });
readerContext.schedulePdfBookSearchPrefetch();
readerContext.updateProgressTools();
if (readerContext.fullSearchInput.value.trim()) readerContext.runFullSearch();
readerContext.scheduleSave();
} catch (error) {
if (readerContext.readerAbortController.signal.aborted || error?.name === "AbortError") return;
console.error(error);
if (
error?.code !== "READER_RESTORE" &&
readerContext.capability.mode === "epub-chapters" &&
readerContext.validFallback(readerContext.fallbackUrl)
) {
const target = new URL(location.href);
target.searchParams.set("url", readerContext.fallbackUrl);
target.searchParams.set("ext", "pdf");
target.searchParams.delete("chapter_manifest");
target.searchParams.delete("fallback");
if (readerContext.isReaderOverlayFrame())
window.parent.postMessage({ type: "voice-reader-open", url: target.href }, location.origin);
else location.replace(target.href);
return;
}
readerContext.fail(
error && error.message ? error.message : "原文件加载失败",
readerContext.classifyReaderError(error, readerContext.capability.mode === "foliate" ? "READER_CORRUPT" : "READER_PARSE")
);
}
}
function syncCapabilityControls() {
readerContext.content.dataset.mode = readerContext.capability.mode || "unsupported";
readerContext.pageInvertToggle.hidden = !["pdf", "pdf-pages"].includes(readerContext.capability.mode);
document.querySelector(".page-controls").hidden = !readerContext.capability.features.pagination;
document.querySelector(".zoom-controls").hidden = !readerContext.capability.features.zoom;
readerContext.mediaTab.hidden = !readerContext.capability.features.media;
readerContext.fullSearchButton.hidden = !readerContext.capability.features.search || (readerContext.capability.mode === "pdf-pages" && !readerContext.ocrManifestUrl && (!readerContext.pdfHybridUrl || readerContext.pdfHybridSearchUnavailable));
readerContext.bookmarkRibbon.hidden = !readerContext.capability.features.bookmarks;
readerContext.readingProgressSummary.querySelector(".reader-progress-bookmark").hidden =
!readerContext.capability.features.bookmarks;
if (
(readerContext.panelState.selected === "media" && readerContext.mediaTab.hidden) ||
(readerContext.panelState.selected === "full-search" && readerContext.fullSearchButton.hidden)
)
readerContext.selectPanel("toc");
}
return {
disposeFormatResources,
registerReaderFormatAdapters,
resolveReaderSource,
start,
syncCapabilityControls
};
}