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:
@@ -1,8 +1,17 @@
|
|||||||
import type { ReaderMetadata } from "../../types/reader";
|
import { getDefaultSettings } from "../settings-manager";
|
||||||
import type { CurrentReader } from "./reader-context";
|
import { getState, setState, getCurrentPage } from "./reader-state";
|
||||||
import { getState, setState } from "./reader-state";
|
|
||||||
import { readerEvents } from "./reader-events";
|
import { readerEvents } from "./reader-events";
|
||||||
import { updateReadingProgress } from "./reader-services";
|
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() {
|
export function createNavigationAPI() {
|
||||||
return {
|
return {
|
||||||
@@ -18,6 +27,25 @@ export function createNavigationAPI() {
|
|||||||
state.currentReader.cif.spine.length - 1
|
state.currentReader.cif.spine.length - 1
|
||||||
) {
|
) {
|
||||||
state.currentReader.currentSpineIndex++;
|
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();
|
renderSpineItem();
|
||||||
}
|
}
|
||||||
} else if (state.currentReader.type === "pdf") {
|
} else if (state.currentReader.type === "pdf") {
|
||||||
@@ -54,6 +82,25 @@ export function createNavigationAPI() {
|
|||||||
if (state.currentReader.type === "ebook") {
|
if (state.currentReader.type === "ebook") {
|
||||||
if (state.currentReader.currentSpineIndex > 0) {
|
if (state.currentReader.currentSpineIndex > 0) {
|
||||||
state.currentReader.currentSpineIndex--;
|
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();
|
renderSpineItem();
|
||||||
}
|
}
|
||||||
} else if (state.currentReader.type === "pdf") {
|
} 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();
|
const state = getState();
|
||||||
if (state.currentReader?.type !== "ebook") return;
|
if (state.currentReader?.type !== "ebook") return;
|
||||||
|
|
||||||
const spineItem =
|
const spineItem =
|
||||||
state.currentReader.cif.spine[state.currentReader.currentSpineIndex];
|
state.currentReader.cif.spine[state.currentReader.currentSpineIndex];
|
||||||
const container = document.getElementById("reader-content");
|
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();
|
applyReaderTheme();
|
||||||
applyTypography();
|
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() {
|
async function renderPDFPage() {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
if (state.currentReader?.type !== "pdf") return;
|
if (state.currentReader?.type !== "pdf") return;
|
||||||
@@ -196,17 +405,15 @@ function renderComicPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyReaderTheme() {
|
function applyReaderTheme() {
|
||||||
const state = getState();
|
const settings = getDefaultSettings();
|
||||||
const settings = getReaderSettings();
|
|
||||||
const container = document.getElementById("reader-content");
|
const container = document.getElementById("reader-content");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
container.className = `ebook-content theme-${settings.reading_theme}`;
|
container.classList.add(`theme-${settings.reading_theme}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTypography() {
|
function applyTypography() {
|
||||||
const state = getState();
|
const settings = getDefaultSettings();
|
||||||
const settings = getReaderSettings();
|
|
||||||
const container = document.getElementById("reader-content");
|
const container = document.getElementById("reader-content");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
@@ -229,11 +436,6 @@ function getFontStack(font: string): string {
|
|||||||
return stacks[font] || stacks["literata"];
|
return stacks[font] || stacks["literata"];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getReaderSettings() {
|
|
||||||
// TODO: Load from settings manager
|
|
||||||
return {} as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendProgressUpdate() {
|
function sendProgressUpdate() {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
if (!state.currentReader || !state.readerMetadata) return;
|
if (!state.currentReader || !state.readerMetadata) return;
|
||||||
@@ -262,5 +464,3 @@ function sendProgressUpdate() {
|
|||||||
|
|
||||||
readerEvents.emit("progressUpdated", { currentPage, totalPages });
|
readerEvents.emit("progressUpdated", { currentPage, totalPages });
|
||||||
}
|
}
|
||||||
|
|
||||||
import { getCurrentPage } from "./reader-state";
|
|
||||||
|
|||||||
Reference in New Issue
Block a user