diff --git a/static/js/app.js b/static/js/app.js index 4ada48a..a99d822 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2467,38 +2467,45 @@ async function parseEpub(arrayBuffer) { }; }); - // 4. Build image map: abs zip path → blob URL + // 4. Build image map: abs zip path → blob URL (parallel) const imageMap = {}; - for (const {href, mediaType} of Object.values(manifest)) { - if (mediaType.startsWith('image/')) { - try { - const buf = await zip.file(href).async('arraybuffer'); - imageMap[href] = URL.createObjectURL(new Blob([buf], {type: mediaType})); - } catch (e) { /* missing asset */ } - } - } + await Promise.all( + Object.values(manifest) + .filter(({mediaType}) => mediaType.startsWith('image/')) + .map(async ({href, mediaType}) => { + try { + const buf = await zip.file(href).async('arraybuffer'); + imageMap[href] = URL.createObjectURL(new Blob([buf], {type: mediaType})); + } catch (e) { /* missing asset */ } + }) + ); // 5. Parse TOC const toc = await _parseEpubToc(zip, opfDoc, manifest); - // 6. Get spine and concatenate chapters + // 6. Get spine items; chapters are returned raw for progressive DOM injection const spineItems = Array.from(opfDoc.querySelectorAll('spine > itemref')) .map(ref => manifest[ref.getAttribute('idref')]?.href) .filter(Boolean); - const parts = []; + const chapters = []; for (let i = 0; i < spineItems.length; i++) { const href = spineItems[i]; try { - const chapterText = await zip.file(href).async('text'); + let chapterText = await zip.file(href).async('text'); + // Strip style/script blocks early — publisher EPUBs often embed large base64 fonts in CSS. + // This shrinks the string before regex and DOMParser work on it. + chapterText = chapterText.replace(/]*>[\s\S]*?<\/style>/gi, ''); + chapterText = chapterText.replace(/]*>[\s\S]*?<\/script>/gi, ''); const chapterDir = href.includes('/') ? href.substring(0, href.lastIndexOf('/') + 1) : ''; const withBlobs = _injectImageBlobs(chapterText, chapterDir, imageMap); const sanitized = sanitizeEpubHtml(withBlobs); - parts.push(`
${sanitized}
`); + chapters.push(`
${sanitized}
`); + if (i % 8 === 0) await new Promise(r => requestAnimationFrame(r)); } catch (e) { /* skip missing */ } } - return {title, author, html: parts.join('\n'), toc, imageMap}; + return {title, author, chapters, toc, imageMap}; } async function _parseEpubToc(zip, opfDoc, manifest) { @@ -2736,6 +2743,7 @@ async function _evictBookCache(bookList) { // Reader settings let readerSettings = { fontSize: 16, lineHeight: 1.8, maxWidth: 65, theme: 'dark', + fontFamily: 'serif', pdfZoom: 100, pdfInverted: false, pdfPaginated: false, pdfSpread: false }; let readerSettingsPanelOpen = false; let currentPdfDoc = null; @@ -3196,7 +3204,18 @@ async function openBook(bookId) { author = result.author || author; toc = result.toc; currentImageMap = result.imageMap; - contentEl.innerHTML = result.html; + // Inject chapters progressively so the browser stays responsive on large books + const BATCH = 8; + for (let i = 0; i < result.chapters.length; i += BATCH) { + const frag = document.createDocumentFragment(); + const tmp = document.createElement('div'); + for (let j = i; j < Math.min(i + BATCH, result.chapters.length); j++) { + tmp.innerHTML = result.chapters[j]; + while (tmp.firstChild) frag.appendChild(tmp.firstChild); + } + contentEl.appendChild(frag); + await new Promise(r => requestAnimationFrame(r)); + } } currentBookToc = toc; @@ -3561,6 +3580,7 @@ function jumpToTocEntry(href) { function loadReaderSettings(bookId) { // Reset to defaults, then apply per-book overrides Object.assign(readerSettings, { fontSize: 16, lineHeight: 1.8, maxWidth: 65, theme: 'dark', + fontFamily: 'serif', pdfZoom: 100, pdfInverted: false, pdfPaginated: false, pdfSpread: false }); try { const saved = JSON.parse(localStorage.getItem(`diora_reader_settings_${bookId}`) || '{}'); @@ -3586,6 +3606,13 @@ function applyReaderSettings(isPdf) { contentEl.style.fontSize = readerSettings.fontSize + 'px'; contentEl.style.lineHeight = readerSettings.lineHeight; contentEl.style.setProperty('--reader-max-width', readerSettings.maxWidth + 'ch'); + const fontMap = { + serif: "Georgia, 'Times New Roman', serif", + sans: 'system-ui, -apple-system, sans-serif', + humanist: 'Verdana, Geneva, sans-serif', + mono: "'Courier New', monospace", + }; + contentEl.style.fontFamily = fontMap[readerSettings.fontFamily] || fontMap.serif; if (_currentPositionAnchor && currentBookId) { requestAnimationFrame(() => restoreFromAnchor($('reader-content'), _currentPositionAnchor)); } @@ -3626,6 +3653,10 @@ function toggleSettingsPanel() { + + + + @@ -3676,6 +3707,15 @@ function toggleSettingsPanel() { saveReaderSettings(); }); + panel.querySelectorAll('[data-rs-font]').forEach(btn => { + btn.addEventListener('click', () => { + readerSettings.fontFamily = btn.dataset.rsFont; + panel.querySelectorAll('[data-rs-font]').forEach(b => b.classList.toggle('active', b === btn)); + applyReaderSettings(false); + saveReaderSettings(); + }); + }); + panel.querySelectorAll('[data-rs-theme]').forEach(btn => { btn.addEventListener('click', () => { readerSettings.theme = btn.dataset.rsTheme;