diff --git a/web/src/reader/core/reader-navigation.ts b/web/src/reader/core/reader-navigation.ts index d10acac..d0b0204 100644 --- a/web/src/reader/core/reader-navigation.ts +++ b/web/src/reader/core/reader-navigation.ts @@ -1,8 +1,17 @@ -import type { ReaderMetadata } from "../../types/reader"; -import type { CurrentReader } from "./reader-context"; -import { getState, setState } from "./reader-state"; +import { getDefaultSettings } from "../settings-manager"; +import { getState, setState, getCurrentPage } from "./reader-state"; import { readerEvents } from "./reader-events"; import { updateReadingProgress } from "./reader-services"; +import { + calculatePagesForEbook, + calculateProgressPercentage, + getCurrentPageFromScroll, + getScrollPositionForPage, + type PageCalculationResult, +} from "../ebook/page-calculator"; + +let pageCalculationResult: PageCalculationResult | null = null; +let isCalculatingPages = false; export function createNavigationAPI() { return { @@ -18,6 +27,25 @@ export function createNavigationAPI() { state.currentReader.cif.spine.length - 1 ) { state.currentReader.currentSpineIndex++; + if (pageCalculationResult) { + const container = document.getElementById("reader-content"); + if (container) { + const viewportHeight = window.innerHeight; + const target = getScrollPositionForPage( + pageCalculationResult, + state.currentReader.currentPage || + state.currentReader.currentSpineIndex + 1, + viewportHeight, + ); + if ( + target && + target.spineIndex !== state.currentReader.currentSpineIndex + ) { + state.currentReader.currentSpineIndex = target.spineIndex; + container.scrollTop = target.scrollTop; + } + } + } renderSpineItem(); } } else if (state.currentReader.type === "pdf") { @@ -54,6 +82,25 @@ export function createNavigationAPI() { if (state.currentReader.type === "ebook") { if (state.currentReader.currentSpineIndex > 0) { state.currentReader.currentSpineIndex--; + if (pageCalculationResult) { + const container = document.getElementById("reader-content"); + if (container) { + const viewportHeight = window.innerHeight; + const target = getScrollPositionForPage( + pageCalculationResult, + state.currentReader.currentPage || + state.currentReader.currentSpineIndex + 1, + viewportHeight, + ); + if ( + target && + target.spineIndex !== state.currentReader.currentSpineIndex + ) { + state.currentReader.currentSpineIndex = target.spineIndex; + container.scrollTop = target.scrollTop; + } + } + } renderSpineItem(); } } else if (state.currentReader.type === "pdf") { @@ -125,20 +172,182 @@ export function createNavigationAPI() { }; } -function renderSpineItem() { +export async function initializePageCalculation() { + readerEvents.on("settings:changed", async (settings: any) => { + const state = getState(); + if (!state.currentReader || state.currentReader.type !== "ebook") return; + + console.log("Recalculating pages due to settings change..."); + + const viewportWidth = window.innerWidth; + pageCalculationResult = await calculatePagesForEbook( + state.currentReader.cif, + viewportWidth, + { + fontSize: settings.font_size, + lineHeight: settings.line_height, + marginWidth: settings.margin_width, + }, + ); + + state.currentReader.pageCalculationResult = pageCalculationResult; + setState({ currentReader: state.currentReader }); + }); + const state = getState(); + if (!state.currentReader || state.currentReader.type !== "ebook") return; + if (isCalculatingPages) return; + + isCalculatingPages = true; + + try { + const settings = getDefaultSettings(); + const viewportWidth = window.innerWidth; + + pageCalculationResult = await calculatePagesForEbook( + state.currentReader.cif, + viewportWidth, + { + fontSize: settings.font_size, + lineHeight: settings.line_height, + marginWidth: settings.margin_width, + }, + ); + + state.currentReader.pageCalculationResult = pageCalculationResult; + setState({ currentReader: state.currentReader }); + + console.log( + "Page calculation complete:", + pageCalculationResult.totalPages, + "pages", + ); + } catch (error) { + console.error("Failed to calculate pages:", error); + } finally { + isCalculatingPages = false; + } +} + +export async function renderSpineItem() { const state = getState(); if (state.currentReader?.type !== "ebook") return; - const spineItem = state.currentReader.cif.spine[state.currentReader.currentSpineIndex]; const container = document.getElementById("reader-content"); - if (!container) return; + if (pageCalculationResult) { + const chapter = pageCalculationResult.chapterMap.get( + state.currentReader.currentSpineIndex, + ); + if (chapter) { + // Reset scroll to start of new chapter + container.scrollTop = 0; + state.currentReader.currentScrollPosition = 0; + } + } + if (!container || !spineItem) return; + // Get the actual content from resources using the href + const resources = state.currentReader.cif.resources; + const contentBlob = resources?.get(spineItem.content); + if (!contentBlob) { + // Fallback: try to fetch directly if not in resources + console.error( + "Spine item content not found in resources:", + spineItem.content, + ); + container.innerHTML = `
Error: Could not load chapter content
`; + container.addEventListener( + "scroll", + () => { + const state = getState(); + if (!state.currentReader || state.currentReader.type !== "ebook") + return; - container.innerHTML = spineItem.content; + state.currentReader.currentScrollPosition = container.scrollTop; + + if (pageCalculationResult) { + const viewportHeight = window.innerHeight; + const currentPage = getCurrentPageFromScroll( + pageCalculationResult, + state.currentReader.currentSpineIndex, + container.scrollTop, + viewportHeight, + ); + + const percentage = calculateProgressPercentage( + pageCalculationResult, + currentPage, + ); + readerEvents.emit("progressUpdated", { + currentPage, + totalPages: pageCalculationResult.totalPages, + percentage, + }); + } + }, + { passive: true }, + ); + return; + } + // Convert blob to text + const contentText = await contentBlob.text(); + + // Rewrite image src paths to use blob URLs from resources + const modifiedContent = rewriteImageUrls(contentText, resources); + + const images = modifiedContent.match(/