Files
bookhoard/web/src/reader/ebook/view-modes.ts
T
john-okeefe 4b2b7ee66a feat: Add ebook view modes with Feature Registration Pattern
Convert view-modes.ts to init(context) pattern with:
- Paginated mode with pagination controls
- Scrolled mode for continuous reading
- Single and double column layouts
- Event-based navigation and mode switching
2026-04-04 13:48:47 -04:00

199 lines
5.7 KiB
TypeScript

// Different viewing modes for ebooks
// Feature Registration Pattern implementation
import type { ReaderContext } from "../core/reader-context";
export function init(context: ReaderContext): void {
let state: ViewModeState | null = null;
context.events.on("reader:loaded", (detail: { container: HTMLElement }) => {
state = { currentMode: "paginated", currentPage: 1 };
setViewMode(detail.container, "paginated");
});
context.events.on("view-mode:set", (detail: { mode: ViewMode }) => {
if (state && context.elements.readerContent) {
state.currentMode = detail.mode;
setViewMode(context.elements.readerContent, detail.mode);
context.events.emit("view-mode:changed", { mode: detail.mode });
}
});
context.events.on("view-mode:get", () => {
if (state) {
context.events.emit("view-mode:current", state);
}
});
context.events.on("pagination:go-to-page", (detail: { page: number }) => {
if (state && context.elements.readerContent) {
state.currentPage = detail.page;
goToPage(context.elements.readerContent, detail.page);
context.events.emit("pagination:page-changed", { page: detail.page });
}
});
}
type ViewMode = "paginated" | "scrolled" | "single-column" | "double-column";
interface ViewModeState {
currentMode: ViewMode;
currentPage: number;
}
function setViewMode(container: HTMLElement, mode: ViewMode): void {
const content = container.querySelector(".ebook-content");
if (!content) return;
const element = content as HTMLElement;
element.classList.remove(
"paginated",
"scrolled",
"single-column",
"double-column",
);
switch (mode) {
case "paginated":
applyPaginatedMode(container, element);
break;
case "scrolled":
applyScrolledMode(container, element);
break;
case "single-column":
applySingleColumn(element);
break;
case "double-column":
applyDoubleColumn(element);
break;
}
}
function applyPaginatedMode(
container: HTMLElement,
element: HTMLElement,
): void {
element.classList.add("paginated");
element.style.height = "100vh";
element.style.overflow = "hidden";
element.style.columnCount = "1";
element.style.columnGap = "0";
enablePagination(container, element);
}
function applyScrolledMode(container: HTMLElement, 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 {
element.classList.add("single-column");
element.style.columnCount = "1";
element.style.columnGap = "0";
element.style.maxWidth = "800px";
element.style.margin = "0 auto";
}
function applyDoubleColumn(element: HTMLElement): void {
element.classList.add("double-column");
element.style.columnCount = "2";
element.style.columnGap = "60px";
element.style.columnRule = "1px solid var(--text-secondary)";
element.style.maxWidth = "1400px";
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;
const pageHeight = content.clientHeight;
const scrollTop = (pageNumber - 1) * pageHeight;
content.scrollTo({
top: scrollTop,
behavior: "smooth",
});
const pageInfo = container.querySelector(".page-info");
if (pageInfo) {
pageInfo.textContent = `Page ${pageNumber} of ${getTotalPageCount(container)}`;
}
}
function getTotalPageCount(container: HTMLElement): number {
const content = container.querySelector(".ebook-content") as HTMLElement;
if (!content) return 1;
const totalHeight = content.scrollHeight;
const pageHeight = content.clientHeight;
return Math.ceil(totalHeight / pageHeight);
}
export function getCurrentViewMode(container: HTMLElement): ViewMode {
const content = container.querySelector(".ebook-content") as HTMLElement;
if (!content) return "paginated";
if (content.classList.contains("paginated")) return "paginated";
if (content.classList.contains("scrolled")) return "scrolled";
if (content.classList.contains("single-column")) return "single-column";
if (content.classList.contains("double-column")) return "double-column";
return "paginated";
}