feat: integrate page calculator with reader navigation

- Add page calculation state and initialization function
- Import page calculator functions for dynamic page tracking
- Update nextPage/previousPage to use getScrollPositionForPage for chapter navigation
- Add scroll tracking in renderSpineItem for real-time page updates
- Emit progressUpdated with dynamic page count instead of spine index
- Fix applyReaderTheme to use classList.add instead of className overwrite
- Remove unused state variables from applyReaderTheme/applyTypography
This commit is contained in:
2026-04-05 21:14:42 -04:00
parent 26c0a2d001
commit edc733947b
+219 -19
View File
@@ -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 = `<p>Error: Could not load chapter content</p>`;
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(/<img[^>]+>/g);
console.log("Found images in content:", images);
// Test resource lookup
console.log("Trying to find image:", {
direct: resources.get("image/1.png"),
withoutFolder: resources.get("1.png"),
allImageKeys: Array.from(resources.keys()).filter(
(k: string) => k.includes("image") || k.includes("png"),
),
});
// Parse HTML and process images BEFORE setting innerHTML
const parser = new DOMParser();
const doc = parser.parseFromString(modifiedContent, "text/html");
const imgElements = Array.from(doc.querySelectorAll("img"));
for (const img of imgElements) {
const src = img.getAttribute("src");
if (!src) continue;
// Try different path formats to find the image
let blob = resources.get(src);
if (!blob) blob = resources.get(src.split("/").pop()); // Try just filename
if (blob) {
// Create blob URL - need to do this properly
const blobUrl = URL.createObjectURL(blob);
img.setAttribute("src", blobUrl);
}
}
container.innerHTML = doc.body.innerHTML;
// Apply reader styling
applyReaderTheme();
applyTypography();
}
function rewriteImageUrls(
htmlContent: string,
resources: Map<string, Blob>,
): string {
// Find all img tags and rewrite their src to blob URLs
const imgRegex = /<img\s+[^>]*src="([^"]+)"[^>]*>/gi;
return htmlContent.replace(imgRegex, (match, src) => {
// Try to find the image in resources
const imageBlob =
resources.get(src) || resources.get(src.replace(/^.*\//, "")); // Try filename only
if (imageBlob) {
const blobUrl = URL.createObjectURL(imageBlob);
return match.replace(src, blobUrl);
}
return match; // Keep original if not found
});
}
async function renderPDFPage() {
const state = getState();
if (state.currentReader?.type !== "pdf") return;
@@ -196,17 +405,15 @@ function renderComicPage() {
}
function applyReaderTheme() {
const state = getState();
const settings = getReaderSettings();
const settings = getDefaultSettings();
const container = document.getElementById("reader-content");
if (!container) return;
container.className = `ebook-content theme-${settings.reading_theme}`;
container.classList.add(`theme-${settings.reading_theme}`);
}
function applyTypography() {
const state = getState();
const settings = getReaderSettings();
const settings = getDefaultSettings();
const container = document.getElementById("reader-content");
if (!container) return;
@@ -229,11 +436,6 @@ function getFontStack(font: string): string {
return stacks[font] || stacks["literata"];
}
function getReaderSettings() {
// TODO: Load from settings manager
return {} as any;
}
function sendProgressUpdate() {
const state = getState();
if (!state.currentReader || !state.readerMetadata) return;
@@ -262,5 +464,3 @@ function sendProgressUpdate() {
readerEvents.emit("progressUpdated", { currentPage, totalPages });
}
import { getCurrentPage } from "./reader-state";