feat: in-app ePub reader with epub.js, TOC sidebar, chapter navigation
Made-with: Cursor
This commit is contained in:
109
templates/reader.html
Normal file
109
templates/reader.html
Normal file
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ title }} — PI Weekly Reader</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<style>
|
||||
body { margin: 0; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
|
||||
.reader-header {
|
||||
display: flex; align-items: center; gap: 1rem;
|
||||
padding: 0.5rem 1rem; border-bottom: 1px solid var(--pico-muted-border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.reader-header h2 { margin: 0; font-size: 1rem; flex: 1; }
|
||||
.reader-body { display: flex; flex: 1; overflow: hidden; }
|
||||
.toc-sidebar {
|
||||
width: 260px; overflow-y: auto; border-right: 1px solid var(--pico-muted-border-color);
|
||||
padding: 1rem; flex-shrink: 0; background: var(--pico-background-color);
|
||||
}
|
||||
.toc-sidebar.collapsed { display: none; }
|
||||
.toc-sidebar h3 { font-size: 0.75rem; text-transform: uppercase; color: var(--pico-muted-color); margin-bottom: 0.5rem; }
|
||||
.toc-item {
|
||||
display: block; padding: 0.4rem 0.5rem; border-radius: var(--pico-border-radius);
|
||||
font-size: 0.85rem; cursor: pointer; text-decoration: none; color: inherit;
|
||||
}
|
||||
.toc-item:hover { background: var(--pico-secondary-hover-background); }
|
||||
.toc-item.active { background: var(--pico-primary-background); color: var(--pico-primary-inverse); }
|
||||
#reader-viewport { flex: 1; overflow: hidden; }
|
||||
.reader-footer {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 0.4rem 1rem; border-top: 1px solid var(--pico-muted-border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.reader-footer small { color: var(--pico-muted-color); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="reader-header">
|
||||
<a href="/issues" class="outline" role="button" style="padding:0.3rem 0.8rem;">← Issues</a>
|
||||
<h2>{{ title }}</h2>
|
||||
<button class="outline" onclick="toggleToc()" style="padding:0.3rem 0.8rem;">TOC</button>
|
||||
</div>
|
||||
<div class="reader-body">
|
||||
<div class="toc-sidebar" id="toc-sidebar">
|
||||
<h3>Table of Contents</h3>
|
||||
<div id="toc-list"></div>
|
||||
</div>
|
||||
<div id="reader-viewport"></div>
|
||||
</div>
|
||||
<div class="reader-footer">
|
||||
<button class="outline" id="prev-btn" onclick="rendition.prev()" style="padding:0.3rem 1rem;">← Previous</button>
|
||||
<small id="chapter-info"></small>
|
||||
<button class="outline" id="next-btn" onclick="rendition.next()" style="padding:0.3rem 1rem;">Next →</button>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/epubjs/dist/epub.min.js"></script>
|
||||
<script>
|
||||
const book = ePub("/issues/{{ issue.id }}/download");
|
||||
const rendition = book.renderTo("reader-viewport", {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
spread: "none",
|
||||
});
|
||||
rendition.display();
|
||||
|
||||
book.loaded.navigation.then(function(nav) {
|
||||
const tocList = document.getElementById("toc-list");
|
||||
nav.toc.forEach(function(chapter, idx) {
|
||||
const item = document.createElement("a");
|
||||
item.className = "toc-item";
|
||||
item.textContent = chapter.label.trim();
|
||||
item.href = "#";
|
||||
item.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
rendition.display(chapter.href);
|
||||
document.querySelectorAll(".toc-item").forEach(i => i.classList.remove("active"));
|
||||
item.classList.add("active");
|
||||
};
|
||||
tocList.appendChild(item);
|
||||
});
|
||||
});
|
||||
|
||||
rendition.on("relocated", function(location) {
|
||||
const current = book.navigation && book.navigation.toc
|
||||
? book.navigation.toc.findIndex(ch => location.start.href.includes(ch.href.split('#')[0]))
|
||||
: -1;
|
||||
const total = book.navigation ? book.navigation.toc.length : 0;
|
||||
if (current >= 0) {
|
||||
document.getElementById("chapter-info").textContent =
|
||||
`Chapter ${current + 1} of ${total}`;
|
||||
document.querySelectorAll(".toc-item").forEach((item, idx) => {
|
||||
item.classList.toggle("active", idx === current);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function(e) {
|
||||
if (e.key === "ArrowLeft") rendition.prev();
|
||||
if (e.key === "ArrowRight") rendition.next();
|
||||
});
|
||||
|
||||
function toggleToc() {
|
||||
document.getElementById("toc-sidebar").classList.toggle("collapsed");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user