feat(ebook-reader): implement page-based pagination with CFI support
This commit implements true page-based pagination for the ebook reader, similar to Kindle's approach where content is split into discrete pages based on viewport size, font settings, and content layout. Phase 1 - Bug Fixes: - Fix goToPage to use getScrollPositionForPage instead of treating page numbers as spine indices - Move page calculation to initialize before first render to avoid race condition with scroll handler - Add fallback page calculation when pageCalculationResult is null Phase 2 - Page Splitting: - Add new page-splitter.ts module with CFI-based splitting for EPUB and height-based fallback for other formats - Integrate splitContent into page-calculator to generate discrete page content for each spine item - Store pages[] array in ChapterPageInfo for rendering - Rewrite navigation (nextPage, previousPage, renderSpineItem) to use page-based approach with currentPage tracking - Remove old scroll-based pagination from view-modes.ts - Update paginated mode CSS for true page clipping Phase 3 - Progress Display: - Update progress-indicator to use currentPage directly instead of calculating from scroll position - Fix getCurrentPage in reader-state to return currentPage for ebooks Phase 4 - Interface Fixes: - Add currentPage field to UniversalReader interface - Update sendProgressUpdate to use currentPage directly - Initialize currentPage to 1 on reader creation Key features: - Dynamic page count based on font size, line height, margins - CFI-based page splitting preserves reading context - Falls back to height-based splitting for non-EPUB formats - Progress display updates immediately on page change - Settings changes trigger page recalculation and re-render
This commit is contained in:
@@ -56,10 +56,10 @@ function setViewMode(container: HTMLElement, mode: ViewMode): void {
|
||||
|
||||
switch (mode) {
|
||||
case "paginated":
|
||||
applyPaginatedMode(container, element);
|
||||
applyPaginatedMode(element);
|
||||
break;
|
||||
case "scrolled":
|
||||
applyScrolledMode(container, element);
|
||||
applyScrolledMode(element);
|
||||
break;
|
||||
case "single-column":
|
||||
applySingleColumn(element);
|
||||
@@ -70,28 +70,56 @@ function setViewMode(container: HTMLElement, mode: ViewMode): void {
|
||||
}
|
||||
}
|
||||
|
||||
function applyPaginatedMode(
|
||||
container: HTMLElement,
|
||||
element: HTMLElement,
|
||||
): void {
|
||||
function applyPaginatedMode(element: HTMLElement): void {
|
||||
element.classList.add("paginated");
|
||||
|
||||
element.style.height = "100vh";
|
||||
// True pagination: content fits exactly in viewport, no scrolling
|
||||
element.style.height = "calc(100vh - 120px)"; // Account for chrome (top 60px + bottom 60px)
|
||||
element.style.overflow = "hidden";
|
||||
element.style.columnCount = "1";
|
||||
element.style.columnGap = "0";
|
||||
element.style.position = "relative";
|
||||
|
||||
enablePagination(container, element);
|
||||
// Inject CSS for page clipping
|
||||
injectPaginatedStyles();
|
||||
}
|
||||
|
||||
function applyScrolledMode(container: HTMLElement, element: HTMLElement): void {
|
||||
function injectPaginatedStyles(): void {
|
||||
// Remove existing injected styles if any
|
||||
const existing = document.getElementById("paginated-styles");
|
||||
existing?.remove();
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = "paginated-styles";
|
||||
style.textContent = `
|
||||
.paginated .ebook-page {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.paginated .ebook-page > * {
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.paginated .ebook-content {
|
||||
height: 100%;
|
||||
overflow: hidden !important;
|
||||
position: relative;
|
||||
}
|
||||
.paginated .ebook-content * {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function applyScrolledMode(element: HTMLElement): void {
|
||||
element.classList.add("scrolled");
|
||||
|
||||
element.style.height = "auto";
|
||||
element.style.overflowY = "auto";
|
||||
element.style.columnCount = "1";
|
||||
|
||||
disablePagination(container);
|
||||
}
|
||||
|
||||
function applySingleColumn(element: HTMLElement): void {
|
||||
@@ -113,51 +141,6 @@ function applyDoubleColumn(element: HTMLElement): void {
|
||||
element.style.margin = "0 auto";
|
||||
}
|
||||
|
||||
function enablePagination(container: HTMLElement, element: HTMLElement): void {
|
||||
const totalHeight = element.scrollHeight;
|
||||
const pageHeight = element.clientHeight;
|
||||
const pageCount = Math.ceil(totalHeight / pageHeight);
|
||||
|
||||
addPaginationControls(container, pageCount);
|
||||
}
|
||||
|
||||
function disablePagination(container: HTMLElement): void {
|
||||
const controls = container.querySelector(".pagination-controls");
|
||||
controls?.remove();
|
||||
}
|
||||
|
||||
function addPaginationControls(
|
||||
container: HTMLElement,
|
||||
pageCount: number,
|
||||
): void {
|
||||
let currentPage = 1;
|
||||
|
||||
const controls = document.createElement("div");
|
||||
controls.className =
|
||||
"pagination-controls fixed bottom-0 left-0 right-0 bg-opacity-95 backdrop-blur border-t";
|
||||
controls.innerHTML = `
|
||||
<button class="prev-page" ${currentPage === 1 ? "disabled" : ""}>← Previous</button>
|
||||
<span class="page-info">Page ${currentPage} of ${pageCount}</span>
|
||||
<button class="next-page" ${currentPage === pageCount ? "disabled" : ""}>Next →</button>
|
||||
`;
|
||||
|
||||
controls.querySelector(".prev-page")?.addEventListener("click", () => {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
goToPage(container, currentPage);
|
||||
}
|
||||
});
|
||||
|
||||
controls.querySelector(".next-page")?.addEventListener("click", () => {
|
||||
if (currentPage < pageCount) {
|
||||
currentPage++;
|
||||
goToPage(container, currentPage);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(controls);
|
||||
}
|
||||
|
||||
function goToPage(container: HTMLElement, pageNumber: number): void {
|
||||
const content = container.querySelector(".ebook-content") as HTMLElement;
|
||||
if (!content) return;
|
||||
@@ -196,4 +179,5 @@ export function getCurrentViewMode(container: HTMLElement): ViewMode {
|
||||
if (content.classList.contains("double-column")) return "double-column";
|
||||
|
||||
return "paginated";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user