Replace PDF.js canvas with native browser iframe PDF viewer
- Browser renders PDF exactly as-is using its built-in viewer (Chrome/Edge/Firefox) - No external dependencies - just a blob URL in an <iframe> - Page navigation via #page=N fragment syncs PDF view with playback position - Fixed flex layout so iframe fills remaining height correctly - Removed PDF.js script tag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
00af54c95a
commit
7afbe3a22a
3 changed files with 51 additions and 67 deletions
|
|
@ -21,9 +21,8 @@ const audioCache = new Map();
|
|||
let allModels = {};
|
||||
let currentUser = null;
|
||||
|
||||
// PDF.js state
|
||||
let pdfDoc = null;
|
||||
const pageCanvases = []; // canvas elements per page
|
||||
// PDF blob URL for native browser viewer
|
||||
let pdfBlobUrl = null;
|
||||
|
||||
// ── DOM ────────────────────────────────────────────────────────────
|
||||
const $ = id => document.getElementById(id);
|
||||
|
|
@ -184,70 +183,45 @@ function renderText(sentences) {
|
|||
// ── VIEW SWITCHING ─────────────────────────────────────────────────
|
||||
function setView(mode) {
|
||||
state.currentView = mode;
|
||||
const textViewer = $('text-viewer');
|
||||
const pdfViewer = $('pdf-viewer');
|
||||
const contentArea = $('content-area');
|
||||
const btnText = $('btn-view-text');
|
||||
const btnPdf = $('btn-view-pdf');
|
||||
|
||||
if (mode === 'pdf') {
|
||||
textViewer.style.display = 'none';
|
||||
pdfViewer.style.display = '';
|
||||
$('text-viewer').style.display = 'none';
|
||||
$('pdf-viewer').style.display = 'flex';
|
||||
contentArea.classList.add('pdf-mode');
|
||||
btnPdf.classList.add('active');
|
||||
btnText.classList.remove('active');
|
||||
$('btn-view-pdf').classList.add('active');
|
||||
$('btn-view-text').classList.remove('active');
|
||||
$('doc-hint').textContent = 'Viewing original PDF · switch to Text to click sentences';
|
||||
} else {
|
||||
textViewer.style.display = '';
|
||||
pdfViewer.style.display = 'none';
|
||||
$('text-viewer').style.display = '';
|
||||
$('pdf-viewer').style.display = 'none';
|
||||
contentArea.classList.remove('pdf-mode');
|
||||
btnText.classList.add('active');
|
||||
btnPdf.classList.remove('active');
|
||||
$('btn-view-text').classList.add('active');
|
||||
$('btn-view-pdf').classList.remove('active');
|
||||
$('doc-hint').textContent = 'Click any sentence to jump there · Space to play/pause · ← → to skip';
|
||||
}
|
||||
}
|
||||
window.setView = setView;
|
||||
|
||||
// ── PDF.js RENDERING ───────────────────────────────────────────────
|
||||
async function renderPDF(b64) {
|
||||
if (typeof pdfjsLib === 'undefined') return;
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc =
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||
|
||||
const pdfPages = $('pdf-pages');
|
||||
pdfPages.innerHTML = '';
|
||||
pageCanvases.length = 0;
|
||||
// ── PDF NATIVE RENDERING ───────────────────────────────────────────
|
||||
function renderPDF(b64) {
|
||||
// Revoke previous blob to free memory
|
||||
if (pdfBlobUrl) { URL.revokeObjectURL(pdfBlobUrl); pdfBlobUrl = null; }
|
||||
|
||||
const binary = atob(b64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
const blob = new Blob([bytes], { type: 'application/pdf' });
|
||||
pdfBlobUrl = URL.createObjectURL(blob);
|
||||
|
||||
pdfDoc = await pdfjsLib.getDocument({ data: bytes }).promise;
|
||||
const pdfPages = $('pdf-pages');
|
||||
pdfPages.innerHTML = '';
|
||||
|
||||
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
||||
const page = await pdfDoc.getPage(i);
|
||||
const vp = page.getViewport({ scale: 1.5 });
|
||||
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'pdf-page-wrap';
|
||||
wrap.dataset.page = i - 1;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = vp.width;
|
||||
canvas.height = vp.height;
|
||||
canvas.style.width = Math.min(vp.width, 800) + 'px';
|
||||
canvas.style.height = (Math.min(vp.width, 800) / vp.width * vp.height) + 'px';
|
||||
|
||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport: vp }).promise;
|
||||
wrap.appendChild(canvas);
|
||||
|
||||
const lbl = document.createElement('div');
|
||||
lbl.className = 'pdf-page-num';
|
||||
lbl.textContent = `Page ${i}`;
|
||||
pdfPages.appendChild(wrap);
|
||||
pdfPages.appendChild(lbl);
|
||||
pageCanvases.push(wrap);
|
||||
}
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = 'pdf-iframe';
|
||||
iframe.src = pdfBlobUrl;
|
||||
iframe.style.cssText = 'width:100%;height:100%;border:none;display:block;';
|
||||
pdfPages.appendChild(iframe);
|
||||
}
|
||||
|
||||
function buildSentencePageMap(pageTexts, sentences) {
|
||||
|
|
@ -269,8 +243,12 @@ function buildSentencePageMap(pageTexts, sentences) {
|
|||
}
|
||||
|
||||
function scrollPDFToPage(pageIdx) {
|
||||
if (pageIdx < 0 || pageIdx >= pageCanvases.length) return;
|
||||
pageCanvases[pageIdx].scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
if (!pdfBlobUrl) return;
|
||||
const iframe = $('pdf-iframe');
|
||||
if (!iframe) return;
|
||||
// #page= fragment navigates inside Chrome/Edge native PDF viewer
|
||||
const target = pdfBlobUrl + '#page=' + (pageIdx + 1);
|
||||
if (iframe.src !== target) iframe.src = target;
|
||||
}
|
||||
|
||||
// ── LOAD DOCUMENT ──────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -219,7 +219,6 @@
|
|||
<p id="loading-text">Loading…</p>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -201,8 +201,12 @@ textarea:focus, .text-field:focus { border-color: var(--accent); }
|
|||
.content-area {
|
||||
flex: 1; overflow-y: auto; padding: 44px 56px;
|
||||
display: flex; flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.content-area.pdf-mode {
|
||||
padding: 20px 20px 0;
|
||||
overflow: hidden; /* iframe fills remaining height */
|
||||
}
|
||||
.content-area.pdf-mode { padding: 24px 24px 0; overflow: hidden; }
|
||||
.document-header { margin-bottom: 28px; }
|
||||
.doc-title-row { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 6px; }
|
||||
.document-title { font-size: 22px; font-weight: 700; flex: 1; }
|
||||
|
|
@ -281,22 +285,25 @@ textarea:focus, .text-field:focus { border-color: var(--accent); }
|
|||
|
||||
/* ── PDF VIEWER ──────────────────────────── */
|
||||
.pdf-viewer {
|
||||
flex: 1; overflow-y: auto; padding: 24px; background: #1a1a1a;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 16px;
|
||||
flex: 1;
|
||||
min-height: 0; /* essential: lets flex child shrink below content size */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
#pdf-pages { display: flex; flex-direction: column; gap: 16px; align-items: center; width: 100%; }
|
||||
.pdf-page-wrap {
|
||||
position: relative; box-shadow: 0 4px 24px rgba(0,0,0,.5);
|
||||
border-radius: 4px; overflow: hidden; max-width: 100%;
|
||||
#pdf-pages {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.pdf-page-wrap canvas { display: block; max-width: 100%; height: auto; }
|
||||
.pdf-page-highlight {
|
||||
position: absolute; border-radius: 2px;
|
||||
background: rgba(124,58,237,.35);
|
||||
pointer-events: none; transition: opacity .2s;
|
||||
}
|
||||
.pdf-page-num {
|
||||
text-align: center; font-size: 11px; color: var(--text-d); margin-top: 4px;
|
||||
#pdf-iframe {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── SETTINGS SIDEBAR ────────────────────── */
|
||||
|
|
|
|||
Loading…
Reference in a new issue