Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions templates/reader.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@
.book-content h1, .book-content h2, .book-content h3 { font-family: -apple-system, sans-serif; margin-top: 1.5em; color: #333; }
.book-content p { margin-bottom: 1.5em; text-align: justify; }

/* Copy Buttons */
.copy-target { position: relative; padding-left: 38px; }
.copy-btn { border: 1px solid #ced4da; background: #fff; color: #495057; border-radius: 999px; width: 28px; height: 28px; font-size: 0.85em; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; transition: background 0.2s, color 0.2s, border-color 0.2s; }
.copy-btn:hover { background: #f1f3f5; }
.copy-btn.copied { background: #d4edda; border-color: #52c41a; color: #2f9e44; }
.copy-btn-inline { position: absolute; left: 0; top: 2px; opacity: 0; pointer-events: none; transition: opacity 0.2s; }
.copy-target:hover .copy-btn-inline:not(.copy-hidden),
.copy-target:focus-within .copy-btn-inline:not(.copy-hidden) { opacity: 1; pointer-events: auto; }
.chapter-copy-bar { display: flex; justify-content: flex-end; margin-bottom: 10px; }
.chapter-copy-btn { width: 34px; height: 34px; font-size: 1em; }

/* Navigation Footer */
.chapter-nav { display: flex; justify-content: space-between; margin-top: 60px; padding-top: 20px; border-top: 1px solid #eee; font-family: -apple-system, sans-serif; }
.nav-btn { text-decoration: none; color: #3498db; font-weight: bold; padding: 10px 20px; border: 1px solid #3498db; border-radius: 4px; transition: all 0.2s; }
Expand Down Expand Up @@ -97,6 +108,15 @@
<!-- MAIN CONTENT -->
<div id="main">
<div class="content-container">
<div class="chapter-copy-bar">
<button
id="copyChapterBtn"
type="button"
class="copy-btn chapter-copy-btn"
aria-label="Copy entire chapter"
title="Copy entire chapter"
>📋</button>
</div>
<div class="book-content">
{{ current_chapter.content | safe }}
</div>
Expand All @@ -122,6 +142,151 @@
</div>

<script>
const COPY_ICON = "⧉";
const SUCCESS_ICON = "✔";
const COPY_RESET_DELAY = 1500;

document.addEventListener("DOMContentLoaded", () => {
const bookContent = document.querySelector(".book-content");
if (!bookContent) {
return;
}

const chapterText = bookContent.innerText.trim();
const headingSections = collectHeadingSections(bookContent);

addParagraphCopyButtons(bookContent);
addHeadingCopyButtons(headingSections);
wireExistingCopyButton(document.getElementById("copyChapterBtn"), () => chapterText);
});

function collectHeadingSections(root) {
const headings = Array.from(root.querySelectorAll("h2, h3"));
return headings.map((heading, index) => {
const nextHeading = headings[index + 1] || null;
const text = extractSectionText(heading, nextHeading).trim();
return { heading, text };
}).filter(section => section.text);
}

function extractSectionText(heading, stopHeading) {
const parts = [heading.innerText.trim()];
let node = heading.nextSibling;
while (node && node !== stopHeading) {
if (node.nodeType === Node.ELEMENT_NODE) {
const text = node.innerText.trim();
if (text) {
parts.push(text);
}
} else if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
if (text) {
parts.push(text);
}
}
node = node.nextSibling;
}
return parts.join("\n\n");
}

function addParagraphCopyButtons(root) {
const paragraphs = root.querySelectorAll("p");
paragraphs.forEach(paragraph => {
const text = paragraph.innerText.trim();
if (!text) {
return;
}
paragraph.classList.add("copy-target");
const button = createCopyButton(() => text, "Copy paragraph");
button.classList.add("copy-btn-inline");
paragraph.appendChild(button);
setupVisibilityReset(button);
});
}

function addHeadingCopyButtons(sections) {
sections.forEach(({ heading, text }) => {
heading.classList.add("copy-target");
const button = createCopyButton(() => text, "Copy section");
button.classList.add("copy-btn-inline");
heading.appendChild(button);
setupVisibilityReset(button);
});
}

function createCopyButton(textProvider, label) {
const button = document.createElement("button");
button.type = "button";
button.className = "copy-btn";
button.textContent = COPY_ICON;
button.setAttribute("aria-label", label);
button.setAttribute("title", label);
wireCopyHandler(button, textProvider);
return button;
}

function wireExistingCopyButton(button, textProvider) {
if (!button || typeof textProvider !== "function") {
return;
}
button.textContent = COPY_ICON;
wireCopyHandler(button, textProvider);
}

function wireCopyHandler(button, textProvider) {
button.addEventListener("click", async () => {
const text = textProvider();
if (!text) {
return;
}

try {
await navigator.clipboard.writeText(text);
showCopySuccess(button);
} catch (err) {
fallbackCopy(text, button);
}
});
}

function fallbackCopy(text, button) {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand("copy");
showCopySuccess(button);
} catch (err) {
console.error("Copy failed", err);
}
document.body.removeChild(textarea);
}

function showCopySuccess(button) {
button.classList.add("copied");
button.textContent = SUCCESS_ICON;
clearTimeout(button._copyResetTimer);
button._copyResetTimer = setTimeout(() => {
button.classList.remove("copied");
button.textContent = COPY_ICON;
button.classList.add("copy-hidden");
}, COPY_RESET_DELAY);
}

function setupVisibilityReset(button) {
const target = button.closest(".copy-target");
if (!target) {
return;
}
const reset = () => button.classList.remove("copy-hidden");
target.addEventListener("mouseleave", reset);
target.addEventListener("focusout", reset);
}

// Helper to map TOC filenames to Spine Indices
// Pass the spine data from python to JS
const spineMap = {
Expand Down