fix: delete issues, ePub reader (JSZip, linear cover, fonts), Pico dialog, UI polish

- Add POST /issues/<id>/delete route with file cleanup
- Fix ePub reader: add JSZip dependency, make cover linear in spine,
  inject system fonts into rendition
- Replace browser confirm() with Pico CSS dialog component
- Fix dashboard button sizing and consistency
- Add favicon, override Pico font stack to suppress Firefox warnings
- Compact issue action buttons

Made-with: Cursor
This commit is contained in:
cottongin
2026-04-06 18:40:04 -04:00
parent c7c6cd979b
commit 872d90d9d9
7 changed files with 221 additions and 55 deletions

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }} — PI Weekly Reader</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📰</text></svg>">
<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>
@@ -50,56 +51,104 @@
<div id="reader-viewport"></div>
</div>
<div class="reader-footer">
<button class="outline" id="prev-btn" onclick="rendition.prev()" style="padding:0.3rem 1rem;">&larr; Previous</button>
<button class="outline" id="prev-btn" onclick="rendition && rendition.prev()" style="padding:0.3rem 1rem;">&larr; Previous</button>
<small id="chapter-info"></small>
<button class="outline" id="next-btn" onclick="rendition.next()" style="padding:0.3rem 1rem;">Next &rarr;</button>
<button class="outline" id="next-btn" onclick="rendition && rendition.next()" style="padding:0.3rem 1rem;">Next &rarr;</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/jszip@3/dist/jszip.min.js"></script>
<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();
let book, rendition;
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);
fetch("/issues/{{ issue.id }}/epub")
.then(r => r.arrayBuffer())
.then(buf => {
book = ePub(buf);
rendition = book.renderTo("reader-viewport", {
width: "100%",
height: "100%",
spread: "none",
});
}
});
document.addEventListener("keydown", function(e) {
if (e.key === "ArrowLeft") rendition.prev();
if (e.key === "ArrowRight") rendition.next();
});
rendition.themes.default({
body: {
"font-family": 'system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif',
"line-height": "1.6",
},
"h1, h2, h3": {
"font-family": 'system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif',
},
});
rendition.display();
book.loaded.navigation.then(function(nav) {
const tocList = document.getElementById("toc-list");
const coverItem = document.createElement("a");
coverItem.className = "toc-item";
coverItem.textContent = "Cover";
coverItem.href = "#";
coverItem.onclick = function(e) {
e.preventDefault();
rendition.display("cover.xhtml");
document.querySelectorAll(".toc-item").forEach(i => i.classList.remove("active"));
coverItem.classList.add("active");
};
tocList.appendChild(coverItem);
nav.toc.forEach(function(chapter) {
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 href = location.start.href;
document.querySelectorAll(".toc-item").forEach(function(item, idx) {
if (idx === 0) {
item.classList.toggle("active", href.includes("cover.xhtml"));
}
});
if (book.navigation && book.navigation.toc) {
const current = book.navigation.toc.findIndex(
ch => href.includes(ch.href.split('#')[0])
);
const total = book.navigation.toc.length;
if (current >= 0) {
document.getElementById("chapter-info").textContent =
`${current + 1} of ${total}`;
document.querySelectorAll(".toc-item").forEach((item, idx) => {
if (idx > 0) item.classList.toggle("active", idx - 1 === current);
else item.classList.remove("active");
});
} else if (href.includes("cover.xhtml")) {
document.getElementById("chapter-info").textContent = "Cover";
} else if (href.includes("nav.xhtml")) {
document.getElementById("chapter-info").textContent = "Table of Contents";
}
}
});
document.addEventListener("keydown", function(e) {
if (e.key === "ArrowLeft" && rendition) rendition.prev();
if (e.key === "ArrowRight" && rendition) rendition.next();
});
})
.catch(err => {
document.getElementById("reader-viewport").innerHTML =
`<p style="padding:2rem;color:red;">Failed to load ePub: ${err.message}</p>`;
});
function toggleToc() {
document.getElementById("toc-sidebar").classList.toggle("collapsed");