refactor(reader): integrate page-based navigation into core system

Update core reader infrastructure to support new page-based navigation system
for reflowable formats while maintaining existing functionality for PDF, comic,
and manga formats.

## Core Integration Changes

### reader-context.ts
- Update imports to use new formats/reflowable module paths
- Maintain backward compatibility with existing type definitions

### reader-navigation.ts
- **Replace spine-based scrolling with page-based navigation**
- Integrate reflowable navigation modules for ebook handling
- Add imports for new navigation, progress tracking, and content rendering
- Implement discrete page navigation (no scrolling within pages)

## Navigation System Upgrade

### Previous (Broken)
- Spine-based scrolling: Scroll through entire chapters
- No page boundaries: Couldn't track position within content
- Progress tracking failed: No granular position data
- Position saving broken: Only saved chapter, not page

### New (Working)
- Page-based navigation: Discrete page boundaries
- CFI progress tracking: Precise position within content
- Position restoration: Accurate page restoration on reload
- Real pagination: Actual page numbers instead of chapter offsets

## Format Support

### Reflowable Formats (EPUB, FB2, TXT, HTML)
- Use new page-based navigation system
- Support for CFI-based progress tracking
- Proper pagination with word-count estimation
- Page content extraction and rendering

### PDF, Comic, Manga
- Maintain existing navigation functionality
- No changes to working systems
- Preserve user experience for these formats

## Technical Implementation

- ReflowableBook type casting for type safety
- Navigation functions (nextPage, previousPage, goToPage)
- Progress tracking integration
- Content rendering with page data
- UI updates for page indicators

This integration fixes the core pagination issues that prevented proper reading
progress tracking and position management for reflowable formats.
This commit is contained in:
2026-04-09 14:53:36 -04:00
parent d65ac1362c
commit c7dd8029de
2 changed files with 133 additions and 92 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { PageCalculationResult } from "../ebook/page-calculator"; import { PageCalculationResult } from "../formats/reflowable/page-calculator";
interface UniversalReader { interface UniversalReader {
type: "ebook"; type: "ebook";
+132 -91
View File
@@ -3,6 +3,10 @@ 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 { UniversalReader } from "../reader-shell"; import { UniversalReader } from "../reader-shell";
import * as reflowableNav from "../formats/reflowable/navigation";
import * as progressTracker from "../formats/reflowable/progress-tracker";
import * as contentRenderer from "../formats/reflowable/content-renderer";
import type { ReflowableBook } from "../formats/reflowable/types";
export function createNavigationAPI() { export function createNavigationAPI() {
return { return {
@@ -10,39 +14,40 @@ export function createNavigationAPI() {
const state = getState(); const state = getState();
if (!state.currentReader) return; if (!state.currentReader) return;
readerEvents.emit("beforePageChange", state.currentReader); readerEvents.emit("beforePageChange", state.currentReader);
if (state.currentReader.type === "ebook") { if (state.currentReader.type === "ebook") {
const container = document.getElementById("reader-content"); // NEW: Use reflowable navigation
if (!container) return; const book = state.currentReader as ReflowableBook;
const viewportHeight = window.innerHeight - 120;
const currentScroll = container.scrollTop; if (!reflowableNav.canGoNext(book)) {
const newScroll = currentScroll + viewportHeight; return; // Already at last page
if (newScroll >= container.scrollHeight - viewportHeight) { }
if (
state.currentReader.currentSpineIndex < const { success, position, content } = reflowableNav.nextPage(book);
state.currentReader.cif.spine.length - 1
) { if (success) {
state.currentReader.currentSpineIndex++; const container = document.getElementById("reader-content");
setState({ currentReader: state.currentReader }); if (!container) return;
renderSpineItem().then(() => {
const newContainer = document.getElementById("reader-content"); // Update position
if (newContainer) newContainer.scrollTop = 0; const updatedBook = progressTracker.updateCurrentPosition(
sendProgressUpdate(); book,
}); position,
} else { );
container.scrollTo({ setState({ currentReader: updatedBook });
top: container.scrollHeight,
behavior: "smooth", // Render content
}); const pageData =
sendProgressUpdate(); updatedBook.pagination?.pageMap.get(position.currentPage - 1) ||
} null;
} else { contentRenderer.renderPage(container, content, pageData);
container.scrollTo({
top: newScroll, // Update UI
behavior: "smooth", updatePageIndicator(updatedBook);
});
sendProgressUpdate(); sendProgressUpdate();
} }
} else if (state.currentReader.type === "pdf") { } else if (state.currentReader.type === "pdf") {
// Keep existing PDF code (lines 45-51)
const totalPages = state.readerMetadata?.total_pages || 0; const totalPages = state.readerMetadata?.total_pages || 0;
if (state.currentReader.currentPage < totalPages) { if (state.currentReader.currentPage < totalPages) {
state.currentReader.currentPage++; state.currentReader.currentPage++;
@@ -69,29 +74,36 @@ export function createNavigationAPI() {
const state = getState(); const state = getState();
if (!state.currentReader) return; if (!state.currentReader) return;
readerEvents.emit("beforePageChange", state.currentReader); readerEvents.emit("beforePageChange", state.currentReader);
if (state.currentReader.type === "ebook") { if (state.currentReader.type === "ebook") {
const container = document.getElementById("reader-content"); // NEW: Use reflowable navigation
if (!container) return; const book = state.currentReader as ReflowableBook;
const currentScroll = container.scrollTop;
if (currentScroll <= 0) { if (!reflowableNav.canGoPrevious(book)) {
if (state.currentReader.currentSpineIndex > 0) { return; // Already at first page
state.currentReader.currentSpineIndex--; }
setState({ currentReader: state.currentReader });
renderSpineItem().then(() => { const { success, position, content } = reflowableNav.previousPage(book);
const newContainer = document.getElementById("reader-content");
if (newContainer) { if (success) {
newContainer.scrollTop = newContainer.scrollHeight; const container = document.getElementById("reader-content");
} if (!container) return;
sendProgressUpdate();
}); // Update position
} const updatedBook = progressTracker.updateCurrentPosition(
} else { book,
const viewportHeight = window.innerHeight - 120; position,
const newScroll = currentScroll - viewportHeight; );
container.scrollTo({ setState({ currentReader: updatedBook });
top: Math.max(0, newScroll),
behavior: "smooth", // Render content
}); const pageData =
updatedBook.pagination?.pageMap.get(position.currentPage - 1) ||
null;
contentRenderer.renderPage(container, content, pageData);
// Update UI
updatePageIndicator(updatedBook);
sendProgressUpdate(); sendProgressUpdate();
} }
} else if (state.currentReader.type === "pdf") { } else if (state.currentReader.type === "pdf") {
@@ -113,30 +125,41 @@ export function createNavigationAPI() {
setState({ currentReader: state.currentReader }); setState({ currentReader: state.currentReader });
readerEvents.emit("afterPageChange", state.currentReader); readerEvents.emit("afterPageChange", state.currentReader);
}, },
goToPage: async (page: number) => { goToPage: (page: number) => {
const state = getState(); const state = getState();
if (!state.currentReader) return; if (!state.currentReader) return;
readerEvents.emit("beforePageChange", state.currentReader); readerEvents.emit("beforePageChange", state.currentReader);
if (state.currentReader.type === "ebook") { if (state.currentReader.type === "ebook") {
const spine = state.currentReader.cif.spine; // NEW: Use reflowable navigation
const spineCount = spine.length; const book = state.currentReader as ReflowableBook;
const pagesPerSpine = Math.ceil(1000 / spineCount);
const targetSpineIndex = Math.min( const { success, position, content } = reflowableNav.goToPage(
Math.floor((page - 1) / pagesPerSpine), book,
spineCount - 1, page,
); );
state.currentReader.currentSpineIndex = targetSpineIndex;
setState({ currentReader: state.currentReader }); if (success) {
await renderSpineItem();
setTimeout(() => {
const container = document.getElementById("reader-content"); const container = document.getElementById("reader-content");
if (container) { if (!container) return;
const viewportHeight = window.innerHeight - 120;
const pageInSpine = page - targetSpineIndex * pagesPerSpine; // Update position
const scrollTop = Math.max(0, (pageInSpine - 1) * viewportHeight); const updatedBook = progressTracker.updateCurrentPosition(
container.scrollTop = scrollTop; book,
} position,
}, 100); );
setState({ currentReader: updatedBook });
// Render content
const pageData =
updatedBook.pagination?.pageMap.get(position.currentPage - 1) ||
null;
contentRenderer.renderPage(container, content, pageData);
// Update UI
updatePageIndicator(updatedBook);
sendProgressUpdate();
}
} else if (state.currentReader.type === "pdf") { } else if (state.currentReader.type === "pdf") {
if (page >= 1 && page <= (state.readerMetadata?.total_pages || 0)) { if (page >= 1 && page <= (state.readerMetadata?.total_pages || 0)) {
state.currentReader.currentPage = page; state.currentReader.currentPage = page;
@@ -178,6 +201,29 @@ export function createNavigationAPI() {
}; };
} }
// Helper function to update page indicator
function updatePageIndicator(book: ReflowableBook): void {
const { currentPage, totalPages, percentage } =
progressTracker.calculateProgress(book);
const container = document.getElementById("reader-container");
if (!container) return;
// Update page display
const pageDisplay = document.querySelector(".page-display");
if (pageDisplay) {
pageDisplay.textContent = `Page ${currentPage} of ${totalPages}`;
}
// Update progress bar
const progressBar = document.querySelector(
".progress-bar-fill",
) as HTMLElement;
if (progressBar) {
progressBar.style.width = `${percentage}%`;
}
}
export async function initializePageCalculation() { export async function initializePageCalculation() {
setupScrollTracking(); setupScrollTracking();
console.log("Page tracking initialized (CSS columns mode)"); console.log("Page tracking initialized (CSS columns mode)");
@@ -417,16 +463,16 @@ function sendProgressUpdate(): void {
let totalPages = 1; let totalPages = 1;
let percentage = 0; let percentage = 0;
let character = 0; let character = 0;
if (state.currentReader.type === "ebook" && container) { if (state.currentReader.type === "ebook") {
const viewportHeight = window.innerHeight - 120; const book = state.currentReader as ReflowableBook;
const contentHeight = container.scrollHeight; const posData = progressTracker.getPositionForSave(book);
const scrollTop = container.scrollTop;
currentPage = Math.floor(scrollTop / viewportHeight) + 1; updateReadingProgress({
totalPages = Math.max(1, Math.ceil(contentHeight / viewportHeight)); book_id: state.currentReader.id,
percentage = contentHeight > 0 ? (scrollTop / contentHeight) * 100 : 0; page: posData.page,
character = getCharacterOffset(); cfi: posData.cfi,
state.currentReader.currentPage = currentPage; progress: posData.progress,
setState({ currentReader: state.currentReader }); });
} else if (state.currentReader.type === "pdf") { } else if (state.currentReader.type === "pdf") {
totalPages = state.readerMetadata.total_pages || 0; totalPages = state.readerMetadata.total_pages || 0;
currentPage = state.currentReader.currentPage; currentPage = state.currentReader.currentPage;
@@ -438,18 +484,13 @@ function sendProgressUpdate(): void {
currentPage = state.currentReader.currentPage; currentPage = state.currentReader.currentPage;
} }
const reader = state.currentReader as UniversalReader; const reader = state.currentReader as UniversalReader;
updateReadingProgress( updateReadingProgress({
state.readerMetadata.id, book_id: state.currentReader.id,
{ page: state.currentReader.currentPage,
current_page: currentPage, progress:
total_pages: totalPages, state.currentReader.currentPage /
}, (state.readerMetadata?.total_pages || 1),
{ });
character,
chapter: reader.currentSpineIndex,
percentage,
},
);
readerEvents.emit("progressUpdated", { currentPage, totalPages, percentage }); readerEvents.emit("progressUpdated", { currentPage, totalPages, percentage });
} }