Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 155 additions & 71 deletions packages/app-expo/assets/reader/reader.html

Large diffs are not rendered by default.

24 changes: 22 additions & 2 deletions packages/app-expo/assets/reader/reader.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,20 @@
if (RN) RN.postMessage(JSON.stringify({ type, ...data }));
}
window.__READANY_READER_BUILD_ID = 'android-local-server-cors';

// ── init error capture (diagnostics for bundle load failures) ──
window.__readerInitErrors = [];
window.addEventListener('error', function (e) {
try {
window.__readerInitErrors.push(String(e && e.message ? e.message : (e.type || 'unknown')) + ' @' + (e.lineno || 0) + ':' + (e.colno || 0));
} catch (err) {}
});
window.addEventListener('unhandledrejection', function (e) {
try {
window.__readerInitErrors.push('Promise rejection: ' + String((e && e.reason && (e.reason.message || e.reason)) || (e && e.reason) || 'unknown'));
} catch (err) {}
});

postToRN('debug', { message: '[ReaderBuild] ' + window.__READANY_READER_BUILD_ID });
Promise.withResolvers ??= function () {
let resolve;
Expand Down Expand Up @@ -1410,6 +1424,12 @@
}
};

if (!window.makeBook) {
var _initErrors = (window.__readerInitErrors || []).join(' | ') || '未捕获到脚本错误';
var _bundleRan = window.__bundleStart ? 'bundle已启动' : 'bundle未启动';
throw new Error('阅读器内核未加载:foliate bundle 未注入或执行失败。[' + _bundleRan + '] ' + _initErrors);
}

let file;
if (msg.base64) {
const binary = atob(msg.base64);
Expand Down Expand Up @@ -1514,7 +1534,7 @@
throw new Error('No book data provided');
}

const book = (file && file.sections) ? file : await makeBook(file);
const book = (file && file.sections) ? file : await window.makeBook(file);
currentBook = book;
attachBookTransformHandler(book);

Expand Down Expand Up @@ -4310,7 +4330,7 @@
return;
}

currentBook = await makeBook(await createBookFileFromMessage(msg));
currentBook = await window.makeBook(await createBookFileFromMessage(msg));
await handleExtractChapters();
} catch (err) {
console.error('[WebView] Error extracting book chapters:', err);
Expand Down
110 changes: 100 additions & 10 deletions packages/app-expo/scripts/build-reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,93 @@ const TEMPLATE = path.resolve(ASSETS_DIR, "reader.template.html");
const OUTPUT = path.resolve(ASSETS_DIR, "reader.html");
const JUSTIFIED_TEXT = path.resolve(ASSETS_DIR, "justified-text.js");

// ES2020+ runtime polyfills for older Android WebViews. Must be ES5-compatible
// (runs before the bundle on devices that may lack modern APIs).
const POLYFILLS = `/* ReadAny WebView runtime polyfills */
(function () {
if (typeof String.prototype.replaceAll !== "function") {
String.prototype.replaceAll = function (search, replace) {
if (search instanceof RegExp) {
if (!search.global) throw new TypeError("String.prototype.replaceAll called with a non-global RegExp");
return this.replace(search, replace);
}
return this.split(String(search)).join(String(replace));
};
}
if (typeof Array.prototype.at !== "function") {
Array.prototype.at = function (index) {
var n = Math.trunc(index) || 0;
if (n < 0) n += this.length;
return n < 0 || n >= this.length ? undefined : this[n];
};
}
if (typeof String.prototype.at !== "function") {
String.prototype.at = function (index) {
var n = Math.trunc(index) || 0;
if (n < 0) n += this.length;
return n < 0 || n >= this.length ? undefined : this[n];
};
}
if (typeof Object.fromEntries !== "function") {
Object.fromEntries = function (entries) {
var obj = {};
for (var i = 0; i < entries.length; i++) {
var kv = entries[i];
obj[kv[0]] = kv[1];
}
return obj;
};
}
if (typeof Promise.allSettled !== "function") {
Promise.allSettled = function (promises) {
return Promise.all(Array.prototype.slice.call(promises).map(function (p) {
return Promise.resolve(p).then(
function (value) { return { status: "fulfilled", value: value }; },
function (reason) { return { status: "rejected", reason: reason }; }
);
}));
};
}
if (typeof structuredClone !== "function") {
(typeof globalThis !== "undefined" ? globalThis : window).structuredClone = function (value) {
return JSON.parse(JSON.stringify(value));
};
}
if (typeof Intl !== "undefined" && typeof Intl.Locale !== "function") {
var ShimLocale = function (tag) {
var parts = String(tag).split("-");
this.language = parts[0] || "";
var dir = "ltr";
if (/^(zh|ja|ko|ar|he|ur|fa)$/i.test(this.language)) dir = "rtl";
this.textInfo = function () { return { direction: dir }; };
this.getTextInfo = this.textInfo;
};
Intl.Locale = ShimLocale;
}
})();
window.__bundleStart = 1;
`;

async function buildReader() {
// Create a temporary entry point
// IMPORTANT: window.makeBook is assigned FIRST and synchronously. Heavy format
// engines (zip/EPUB/PDF) are loaded lazily in a non-blocking promise chain so a
// failure in one of them (e.g. pdf.js on an old WebView) can never prevent the
// reader kernel from becoming available.
const entryContent = `
import { makeBook, View } from "${FOLIATE_DIR.replace(/\\/g, "/")}/view.js";
import { Overlayer } from "${FOLIATE_DIR.replace(/\\/g, "/")}/overlayer.js";
import * as CFI from "${FOLIATE_DIR.replace(/\\/g, "/")}/epubcfi.js";
import { configure, ZipReader, BlobReader, TextWriter, BlobWriter } from "${FOLIATE_DIR.replace(/\\/g, "/")}/vendor/zip.js";
import { EPUB } from "${FOLIATE_DIR.replace(/\\/g, "/")}/epub.js";
import { extractPDFChapters, makePDFFromURL } from "${FOLIATE_DIR.replace(/\\/g, "/")}/pdf.js";

window.makeBook = makeBook;
window.Overlayer = Overlayer;
window.CFI = CFI;

// Expose zip.js and EPUB for lazy Range-based loading in reader template
window._zipJs = { configure, ZipReader, BlobReader, TextWriter, BlobWriter };
window._EPUB = EPUB;
window._makePDFFromURL = makePDFFromURL;
window._extractPDFChapters = extractPDFChapters;
// Placeholders — filled in by the async engine loader below
window._zipJs = null;
window._EPUB = null;
window._makePDFFromURL = null;
window._extractPDFChapters = null;

if (!customElements.get('foliate-view')) {
customElements.define('foliate-view', View);
Expand All @@ -41,6 +109,28 @@ async function buildReader() {
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'foliate-loaded' }));
}

// Lazy-load zip.js / EPUB / PDF engines without blocking the reader kernel.
// Each import is converted by esbuild to a Promise-based lazy require, so a
// failing engine (e.g. pdf.js on an old WebView) is contained here and the
// reader still works — it only loses lazy Range loading / PDF features.
Promise.resolve()
.then(() => import("${FOLIATE_DIR.replace(/\\/g, "/")}/vendor/zip.js"))
.then((m) => {
window._zipJs = { configure: m.configure, ZipReader: m.ZipReader, BlobReader: m.BlobReader, TextWriter: m.TextWriter, BlobWriter: m.BlobWriter };
return import("${FOLIATE_DIR.replace(/\\/g, "/")}/epub.js");
})
.then((m) => {
window._EPUB = m.EPUB;
return import("${FOLIATE_DIR.replace(/\\/g, "/")}/pdf.js");
})
.then((m) => {
window._makePDFFromURL = m.makePDFFromURL;
window._extractPDFChapters = m.extractPDFChapters;
})
.catch((err) => {
try { console.warn('[Reader] Lazy engine init failed:', err); } catch (_) {}
});
`;

const entryFile = path.resolve(__dirname, "../.foliate-entry.mjs");
Expand All @@ -51,13 +141,13 @@ async function buildReader() {
entryPoints: [entryFile],
bundle: true,
format: "iife",
target: "es2020",
target: "es2017",
minify: true,
write: false,
resolveExtensions: [".js", ".mjs"],
});

const bundledJS = result.outputFiles[0].text;
const bundledJS = POLYFILLS + "\n" + result.outputFiles[0].text;

// Read the template HTML and reader-side helper sources (never modified)
const template = fs.readFileSync(TEMPLATE, "utf-8");
Expand Down