make sure pdf viewer builds
This commit is contained in:
1 parent
32ae81cb20
commit
1dae31e2bc
6 files changed
+292
-40
No files matched your search
@@ -62,4 +62,10 @@ export default defineConfig({
|
||||
md.use(mdInline);
|
||||
},
|
||||
},
|
||||
vite: {
|
||||
ssr: {
|
||||
noExternal: [],
|
||||
external: ["pdfjs-dist"], // don't bundle it for SSR — leave it browser-only
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,22 +1,279 @@
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<VuePdf v-for="page in pages" :key="page" :pdf="pdf" :page="page" />
|
||||
<div class="pdf-viewer">
|
||||
<!-- Toolbar -->
|
||||
<div class="pdf-toolbar">
|
||||
<span class="pdf-toolbar__name">{{ fileName || src }}</span>
|
||||
<div class="pdf-toolbar__right">
|
||||
<span class="pdf-toolbar__pages" v-if="totalPages">
|
||||
{{ totalPages }} pages
|
||||
</span>
|
||||
<button class="pdf-zoom-btn" @click="zoom(-0.25)" title="Zoom out">
|
||||
−
|
||||
</button>
|
||||
<span class="pdf-zoom-label">{{ Math.round(scale * 100) }}%</span>
|
||||
<button class="pdf-zoom-btn" @click="zoom(+0.25)" title="Zoom in">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scroll container — all pages rendered vertically -->
|
||||
<div class="pdf-scroll" ref="scrollEl">
|
||||
<div v-if="loading" class="pdf-msg">Loading…</div>
|
||||
<div v-else-if="error" class="pdf-msg pdf-msg--err">{{ error }}</div>
|
||||
<div v-else class="pdf-pages">
|
||||
<canvas
|
||||
v-for="n in totalPages"
|
||||
:key="n"
|
||||
:ref="(el) => setCanvasRef(el, n)"
|
||||
class="pdf-page"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ src: string }>();
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from "vue";
|
||||
|
||||
import { VuePdf, usePDF } from "@vue3-office/vue-pdf";
|
||||
import "@vue3-office/vue-pdf/style.css";
|
||||
const props = defineProps({
|
||||
/** URL string, Uint8Array, or ArrayBuffer */
|
||||
src: { type: [String, Object], required: true },
|
||||
fileName: { type: String },
|
||||
initialScale: { type: Number, default: 1.25 },
|
||||
});
|
||||
|
||||
const { pdf, pages, info } = usePDF(props.src);
|
||||
// ── state ──────────────────────────────────────────────────────
|
||||
const scrollEl = ref(null);
|
||||
const loading = ref(true);
|
||||
const error = ref(null);
|
||||
const totalPages = ref(0);
|
||||
const scale = ref(props.initialScale);
|
||||
|
||||
let pdfjsLib = null;
|
||||
let pdfDoc = null;
|
||||
const canvasMap = {}; // pageNum → <canvas> element
|
||||
const taskMap = {}; // pageNum → active renderTask
|
||||
|
||||
// ── canvas ref collector ───────────────────────────────────────
|
||||
function setCanvasRef(el, n) {
|
||||
if (el) canvasMap[n] = el;
|
||||
}
|
||||
|
||||
// ── load ───────────────────────────────────────────────────────
|
||||
async function loadPdf(src) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
totalPages.value = 0;
|
||||
|
||||
try {
|
||||
if (!pdfjsLib) {
|
||||
// Dynamic import — never runs in Node/SSR
|
||||
pdfjsLib = await import("pdfjs-dist");
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
"pdfjs-dist/build/pdf.worker.mjs",
|
||||
import.meta.url,
|
||||
).toString();
|
||||
}
|
||||
|
||||
if (pdfDoc) {
|
||||
pdfDoc.destroy();
|
||||
pdfDoc = null;
|
||||
}
|
||||
|
||||
pdfDoc = await pdfjsLib.getDocument(src).promise;
|
||||
totalPages.value = pdfDoc.numPages;
|
||||
loading.value = false;
|
||||
|
||||
// Wait for Vue to stamp out all the <canvas> elements
|
||||
await nextTick();
|
||||
await renderAll();
|
||||
} catch (e) {
|
||||
error.value = `Failed to load PDF: ${e.message}`;
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── render all pages ───────────────────────────────────────────
|
||||
async function renderAll() {
|
||||
// Cancel any in-flight tasks first
|
||||
for (const task of Object.values(taskMap)) {
|
||||
try {
|
||||
task.cancel();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Render pages sequentially — avoids memory thrash on mobile/Android
|
||||
for (let n = 1; n <= totalPages.value; n++) {
|
||||
await renderPage(n);
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPage(n) {
|
||||
if (!pdfDoc) return;
|
||||
const canvas = canvasMap[n];
|
||||
if (!canvas) return;
|
||||
|
||||
try {
|
||||
const page = await pdfDoc.getPage(n);
|
||||
const viewport = page.getViewport({ scale: scale.value });
|
||||
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
const task = page.render({
|
||||
canvasContext: canvas.getContext("2d"),
|
||||
viewport,
|
||||
});
|
||||
taskMap[n] = task;
|
||||
await task.promise;
|
||||
delete taskMap[n];
|
||||
} catch (e) {
|
||||
if (e?.name !== "RenderingCancelledException") {
|
||||
console.warn(`Page ${n} render error:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── zoom ───────────────────────────────────────────────────────
|
||||
async function zoom(delta) {
|
||||
scale.value = Math.min(4, Math.max(0.5, +(scale.value + delta).toFixed(2)));
|
||||
await nextTick();
|
||||
await renderAll();
|
||||
}
|
||||
|
||||
// ── lifecycle ──────────────────────────────────────────────────
|
||||
onMounted(() => loadPdf(props.src));
|
||||
|
||||
watch(
|
||||
() => props.src,
|
||||
(src) => loadPdf(src),
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
for (const task of Object.values(taskMap)) {
|
||||
try {
|
||||
task.cancel();
|
||||
} catch {}
|
||||
}
|
||||
if (pdfDoc) pdfDoc.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
.container {
|
||||
width: 100%;
|
||||
<style scoped>
|
||||
/* ── root ── */
|
||||
.pdf-viewer {
|
||||
--bar-bg: #141414;
|
||||
--bg: #1e1e1e;
|
||||
--border: #2e2e2e;
|
||||
--text: #d4d4d4;
|
||||
--muted: #666;
|
||||
--accent: #5ba3f5;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%; /* parent must supply a bounded height */
|
||||
max-height: 80vh;
|
||||
overflow: scroll;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
/* font-family: "JetBrains Mono", "Fira Mono", monospace; */
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* ── toolbar ── */
|
||||
.pdf-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
padding: 0 14px;
|
||||
height: 40px;
|
||||
background: var(--bar-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pdf-toolbar__name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pdf-toolbar__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-toolbar__pages {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.pdf-zoom-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 4px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
color 0.15s;
|
||||
}
|
||||
|
||||
.pdf-zoom-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.pdf-zoom-label {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── scroll area — the fix lives here ── */
|
||||
.pdf-scroll {
|
||||
flex: 1;
|
||||
min-height: 0; /* lets the flex child shrink so overflow-y activates */
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
padding: 20px 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/* ── pages stack ── */
|
||||
.pdf-pages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pdf-page {
|
||||
display: block;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.6);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ── status ── */
|
||||
.pdf-msg {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.pdf-msg--err {
|
||||
color: #e06c6c;
|
||||
}
|
||||
</style>
|
||||
@@ -1041,7 +1041,7 @@ That’s it for this lesson! You are progressing really well in Korean! There ar
|
||||
[Okay, I got it! Take me to the next lesson!](./07-irregulars.md) Or,
|
||||
[Click here for a workbook to go along with this lesson.](https://www.howtostudykorean.com/workbooks#Unit118)
|
||||
|
||||
---
|
||||
## Resources
|
||||
|
||||
<YouTube id=LWSbh7Og2Sg />
|
||||
|
||||
|
||||
@@ -1390,6 +1390,8 @@ For now, focus on the concepts that were introduced in this lesson. When you rea
|
||||
|
||||
Everybody is confused when they learn these irregulars. Eventually you will reach a point where all of these will come natural to you. Whenever you learn a new word where the stem ends in ㅅ/ㄹ/ㅂ/ㄷ/르/ㅡ just make a mental note about how you should conjugate that word in the future. I don’t even have to think about these irregulars anymore because they just flow out naturally. If you can’t memorize them all right now, just try to understand them, which will allow you to recognize them later. Eventually, you will memorize them simply from using and hearing them so much.
|
||||
|
||||
## Resources
|
||||
|
||||
These videos are available to reinforce this lesson:
|
||||
|
||||
[Sentence Practice]
|
||||
|
||||
Reference in new issue
Block a user