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:
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}PI Weekly{% endblock %} — Plymouth Independent</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') }}">
|
||||
</head>
|
||||
@@ -36,6 +37,48 @@
|
||||
<footer class="container">
|
||||
<small>PI Weekly Newspaper Generator</small>
|
||||
</footer>
|
||||
<dialog id="confirm-dialog">
|
||||
<article>
|
||||
<header>
|
||||
<button aria-label="Close" rel="prev" onclick="closeConfirm()"></button>
|
||||
<p><strong id="confirm-title">Confirm</strong></p>
|
||||
</header>
|
||||
<p id="confirm-message"></p>
|
||||
<footer>
|
||||
<button class="secondary" onclick="closeConfirm()">Cancel</button>
|
||||
<button id="confirm-ok" onclick="submitConfirm()">Confirm</button>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
<script>
|
||||
let _confirmTarget = null;
|
||||
function confirmAction(message, formOrEl, title) {
|
||||
_confirmTarget = formOrEl;
|
||||
document.getElementById('confirm-title').textContent = title || 'Confirm';
|
||||
document.getElementById('confirm-message').textContent = message;
|
||||
const btn = document.getElementById('confirm-ok');
|
||||
btn.className = '';
|
||||
if (formOrEl.dataset.confirmDanger) {
|
||||
btn.className = 'btn-danger-fill';
|
||||
}
|
||||
const dialog = document.getElementById('confirm-dialog');
|
||||
document.documentElement.classList.add('modal-is-open', 'modal-is-opening');
|
||||
dialog.showModal();
|
||||
setTimeout(() => document.documentElement.classList.remove('modal-is-opening'), 200);
|
||||
}
|
||||
function closeConfirm() {
|
||||
const dialog = document.getElementById('confirm-dialog');
|
||||
document.documentElement.classList.add('modal-is-closing');
|
||||
setTimeout(() => {
|
||||
dialog.close();
|
||||
document.documentElement.classList.remove('modal-is-open', 'modal-is-closing');
|
||||
_confirmTarget = null;
|
||||
}, 200);
|
||||
}
|
||||
function submitConfirm() {
|
||||
if (_confirmTarget) _confirmTarget.submit();
|
||||
}
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<button type="submit">Fetch Now</button>
|
||||
</form>
|
||||
<a href="/articles" role="button" class="outline">View</a>
|
||||
<a href="/publish" role="button">Publish New</a>
|
||||
<a href="/publish" role="button" class="outline">Publish New</a>
|
||||
</div>
|
||||
|
||||
{% if latest_issue %}
|
||||
|
||||
@@ -26,16 +26,16 @@
|
||||
<td>{{ item.article_count }}</td>
|
||||
<td>{{ item.issue.cover_method }}</td>
|
||||
<td>{{ item.issue.created_at.strftime('%b %d, %Y %H:%M') }}</td>
|
||||
<td>
|
||||
<a href="/issues/{{ item.issue.id }}/read" role="button" class="outline">
|
||||
Read
|
||||
</a>
|
||||
<a href="/issues/{{ item.issue.id }}/download" role="button" class="outline">
|
||||
Download
|
||||
</a>
|
||||
<form method="post" action="/issues/{{ item.issue.id }}/regenerate" style="display: inline;">
|
||||
<td class="issue-actions">
|
||||
<a href="/issues/{{ item.issue.id }}/read" role="button" class="outline">Read</a>
|
||||
<a href="/issues/{{ item.issue.id }}/download" role="button" class="outline">Download</a>
|
||||
<form method="post" action="/issues/{{ item.issue.id }}/regenerate">
|
||||
<button type="submit" class="outline contrast">Regenerate</button>
|
||||
</form>
|
||||
<form method="post" action="/issues/{{ item.issue.id }}/delete" data-confirm-danger="true"
|
||||
onsubmit="event.preventDefault(); confirmAction('The ePub and cover files will also be removed.', this, 'Delete Issue?');">
|
||||
<button type="submit" class="outline btn-danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -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;">← Previous</button>
|
||||
<button class="outline" id="prev-btn" onclick="rendition && 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>
|
||||
<button class="outline" id="next-btn" onclick="rendition && rendition.next()" style="padding:0.3rem 1rem;">Next →</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");
|
||||
|
||||
Reference in New Issue
Block a user