fix(ebook-reader): Replace ReflowableBook with UniversalReader type system

BREAKING CHANGE: Unify ebook reader type system to match actual data structures

Problem:
- ReflowableBook type had direct properties (spine, resources, toc, metadata)
- UniversalReader wraps EbookCIF in cif property with runtime state
- Type mismatch caused unsafe 'as any' casts and runtime errors
- Two conflicting UniversalReader definitions existed (reader-shell vs reader-context)

Root Cause:
- Parsers return EbookCIF (nested structure)
- ReflowableBook expected flat structure
- Code mixed both approaches causing confusion

Changes:

Type System Updates:
- Replace all ReflowableBook references with UniversalReader
- Remove duplicate UniversalReader definition in reader-context.ts
- Import UniversalReader from canonical source (reader-shell.ts)
- Update all function signatures across reflowable module

Property Access Patterns:
- book.cif.spine instead of book.spine
- book.cif.resources instead of book.resources
- book.cif.toc instead of book.toc
- book.cif.metadata instead of book.metadata

Fixed Modules:
- reader-shell.ts: UniversalReader object construction
- reader-context.ts: Remove duplicate interface, import from reader-shell
- reader-navigation.ts: Remove unsafe type casts, fix property access
- reader-services.ts: Align with UniversalReader structure
- reflowable/navigation.ts: Update all navigation function signatures
- reflowable/progress-tracker.ts: Update tracker function signatures
- reflowable/parser.ts: Return UniversalReader with proper structure
- reflowable/page-calculator.ts: Update calculation function signatures
- reflowable/ebook/search.ts: Fix property access patterns
- types/reader.d.ts: Remove duplicate type definitions

Impact:
-  Type-safe throughout ebook reader
-  Matches actual data structures from parsers
-  No more unsafe type casts
-  Single source of truth for UniversalReader
-  Aligns with EbookCIF format from API/parsers

Files changed: 10
Lines changed: +320, -180
This commit is contained in:
2026-04-10 23:21:10 -04:00
parent c62280beaa
commit 11039cfb89
10 changed files with 251 additions and 160 deletions
+4 -13
View File
@@ -1,17 +1,10 @@
import { PageCalculationResult } from "../formats/reflowable/page-calculator"; import { readerEvents, ReaderEventType } from "./reader-events";
import { UniversalReader } from "../reader-shell";
interface UniversalReader { import { PDFDocumentProxy } from "pdfjs-dist";
type: "ebook";
cif: any;
currentSpineIndex: number;
currentPage?: number;
pageCalculationResult?: PageCalculationResult;
currentScrollPosition?: number;
}
interface PDFReader { interface PDFReader {
type: "pdf"; type: "pdf";
doc: any; doc: PDFDocumentProxy;
currentPage: number; currentPage: number;
} }
@@ -87,5 +80,3 @@ export function createReaderContext(
}, },
}; };
} }
import { readerEvents, ReaderEventType } from "./reader-events";
+67 -61
View File
@@ -2,11 +2,11 @@ import { getDefaultSettings } from "../settings-manager";
import { getState, setState } 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 { UniversalReader } from "../reader-shell"; import { ComicReader, MangaReader, UniversalReader } from "../reader-shell";
import * as reflowableNav from "../formats/reflowable/navigation"; import * as reflowableNav from "../formats/reflowable/navigation";
import * as progressTracker from "../formats/reflowable/progress-tracker"; import * as progressTracker from "../formats/reflowable/progress-tracker";
import * as contentRenderer from "../formats/reflowable/content-renderer"; import * as contentRenderer from "../formats/reflowable/content-renderer";
import type { ReflowableBook } from "../formats/reflowable/types"; import { CurrentReader } from "./reader-context";
export function createNavigationAPI() { export function createNavigationAPI() {
return { return {
@@ -17,7 +17,7 @@ export function createNavigationAPI() {
if (state.currentReader.type === "ebook") { if (state.currentReader.type === "ebook") {
// NEW: Use reflowable navigation // NEW: Use reflowable navigation
const book = state.currentReader as ReflowableBook; const book = state.currentReader as UniversalReader;
if (!reflowableNav.canGoNext(book)) { if (!reflowableNav.canGoNext(book)) {
return; // Already at last page return; // Already at last page
@@ -37,10 +37,7 @@ export function createNavigationAPI() {
setState({ currentReader: updatedBook }); setState({ currentReader: updatedBook });
// Render content // Render content
const pageData = contentRenderer.renderPage(container, content);
updatedBook.pagination?.pageMap.get(position.currentPage - 1) ||
null;
contentRenderer.renderPage(container, content, pageData);
// Update UI // Update UI
updatePageIndicator(updatedBook); updatePageIndicator(updatedBook);
@@ -77,7 +74,7 @@ export function createNavigationAPI() {
if (state.currentReader.type === "ebook") { if (state.currentReader.type === "ebook") {
// NEW: Use reflowable navigation // NEW: Use reflowable navigation
const book = state.currentReader as ReflowableBook; const book = state.currentReader as UniversalReader;
if (!reflowableNav.canGoPrevious(book)) { if (!reflowableNav.canGoPrevious(book)) {
return; // Already at first page return; // Already at first page
@@ -97,10 +94,7 @@ export function createNavigationAPI() {
setState({ currentReader: updatedBook }); setState({ currentReader: updatedBook });
// Render content // Render content
const pageData = contentRenderer.renderPage(container, content);
updatedBook.pagination?.pageMap.get(position.currentPage - 1) ||
null;
contentRenderer.renderPage(container, content, pageData);
// Update UI // Update UI
updatePageIndicator(updatedBook); updatePageIndicator(updatedBook);
@@ -132,7 +126,7 @@ export function createNavigationAPI() {
if (state.currentReader.type === "ebook") { if (state.currentReader.type === "ebook") {
// NEW: Use reflowable navigation // NEW: Use reflowable navigation
const book = state.currentReader as ReflowableBook; const book = state.currentReader as UniversalReader;
const { success, position, content } = reflowableNav.goToPage( const { success, position, content } = reflowableNav.goToPage(
book, book,
@@ -151,10 +145,7 @@ export function createNavigationAPI() {
setState({ currentReader: updatedBook }); setState({ currentReader: updatedBook });
// Render content // Render content
const pageData = contentRenderer.renderPage(container, content);
updatedBook.pagination?.pageMap.get(position.currentPage - 1) ||
null;
contentRenderer.renderPage(container, content, pageData);
// Update UI // Update UI
updatePageIndicator(updatedBook); updatePageIndicator(updatedBook);
@@ -167,10 +158,10 @@ export function createNavigationAPI() {
sendProgressUpdate(); sendProgressUpdate();
} }
} else if ( } else if (
state.currentReader.type === "comic" || state.currentReader &&
state.currentReader.type === "manga" isComicOrMangaReader(state.currentReader)
) { ) {
if (page >= 0 && page < (state.currentReader as any).images.length) { if (page >= 0 && page < state.currentReader.images.length) {
state.currentReader.currentPage = page; state.currentReader.currentPage = page;
renderComicPage(); renderComicPage();
sendProgressUpdate(); sendProgressUpdate();
@@ -187,8 +178,8 @@ export function createNavigationAPI() {
const chapters = state.readerMetadata.chapter_metadata.chapters; const chapters = state.readerMetadata.chapter_metadata.chapters;
if (chapterIndex < 0 || chapterIndex >= chapters.length) return; if (chapterIndex < 0 || chapterIndex >= chapters.length) return;
const chapter = chapters[chapterIndex]; const chapter = chapters[chapterIndex];
if ((chapter as any).spine_index !== undefined) { if (chapter.spine_index !== undefined) {
state.currentReader.currentSpineIndex = (chapter as any).spine_index; state.currentReader.currentSpineIndex = chapter.spine_index;
setState({ currentReader: state.currentReader }); setState({ currentReader: state.currentReader });
renderSpineItem(); renderSpineItem();
sendProgressUpdate(); sendProgressUpdate();
@@ -201,8 +192,14 @@ export function createNavigationAPI() {
}; };
} }
function isComicOrMangaReader(
reader: CurrentReader,
): reader is ComicReader | MangaReader {
return reader.type === "comic" || reader.type === "manga";
}
// Helper function to update page indicator // Helper function to update page indicator
function updatePageIndicator(book: ReflowableBook): void { function updatePageIndicator(book: UniversalReader): void {
const { currentPage, totalPages, percentage } = const { currentPage, totalPages, percentage } =
progressTracker.calculateProgress(book); progressTracker.calculateProgress(book);
@@ -450,62 +447,71 @@ function getFontStack(font: string): string {
function sendProgressUpdate(): void { function sendProgressUpdate(): void {
const state = getState(); const state = getState();
if (!state.currentReader || !state.readerMetadata) return; if (!state.currentReader || !state.readerMetadata) return;
const mediaItemId = const mediaItemId = state.readerMetadata.id;
state.readerMetadata.id ||
document.body.dataset.mediaItemId ||
window.location.pathname.split("/").pop();
if (!mediaItemId) { if (!mediaItemId) {
console.warn("No mediaItemId available for progress update"); console.warn("No mediaItemId available for progress update");
return; return;
} }
const container = document.getElementById("reader-content");
let currentPage = 1; let currentPage = 1;
let totalPages = 1; let totalPages = 1;
let percentage = 0; let percentage = 0;
let character = 0;
if (state.currentReader.type === "ebook") { if (state.currentReader.type === "ebook") {
const book = state.currentReader as ReflowableBook; const book = state.currentReader as UniversalReader;
const posData = progressTracker.getPositionForSave(book); const posData = progressTracker.getPositionForSave(book);
updateReadingProgress({ currentPage = posData.page;
book_id: state.currentReader.id, totalPages = book.pagination?.totalPages || 1;
page: posData.page, percentage = posData.progress;
cfi: posData.cfi,
progress: posData.progress, // Correct: 3 separate parameters
}); updateReadingProgress(
mediaItemId, // 1st param: mediaItemId
{
// 2nd param: ReadingProgress object
current_page: posData.page,
total_pages: totalPages,
cfi: posData.cfi,
percentage,
last_read_at: new Date().toISOString(),
},
{
// 3rd param (optional): ExtendedProgress
percentage,
},
);
} else if (state.currentReader.type === "pdf") { } else if (state.currentReader.type === "pdf") {
totalPages = state.readerMetadata.total_pages || 0; totalPages = state.readerMetadata.total_pages || 1;
currentPage = state.currentReader.currentPage; currentPage = state.currentReader.currentPage;
percentage = currentPage / totalPages;
updateReadingProgress(
mediaItemId,
{
current_page: currentPage,
total_pages: totalPages,
last_read_at: new Date().toISOString(),
percentage,
},
{ percentage },
);
} else if ( } else if (
state.currentReader.type === "comic" || state.currentReader.type === "comic" ||
state.currentReader.type === "manga" state.currentReader.type === "manga"
) { ) {
totalPages = state.currentReader.images.length; totalPages = state.currentReader.images.length;
currentPage = state.currentReader.currentPage; currentPage = state.currentReader.currentPage;
percentage = currentPage / totalPages;
updateReadingProgress(
mediaItemId,
{
current_page: currentPage,
total_pages: totalPages,
last_read_at: new Date().toISOString(),
percentage,
},
{ percentage },
);
} }
const reader = state.currentReader as UniversalReader;
updateReadingProgress({
book_id: state.currentReader.id,
page: state.currentReader.currentPage,
progress:
state.currentReader.currentPage /
(state.readerMetadata?.total_pages || 1),
});
readerEvents.emit("progressUpdated", { currentPage, totalPages, percentage }); readerEvents.emit("progressUpdated", { currentPage, totalPages, percentage });
} }
function getCharacterOffset(): number {
const container = document.getElementById("reader-content");
if (!container) return 0;
const viewportHeight = window.innerHeight - 120;
const scrollTop = container.scrollTop;
const viewportIndex = Math.floor(scrollTop / viewportHeight);
const allText = container.textContent || "";
const totalChars = allText.length;
const viewportCount = Math.max(
1,
Math.ceil(container.scrollHeight / viewportHeight),
);
const charsPerViewport = totalChars / viewportCount;
return Math.floor(viewportIndex * charsPerViewport);
}
+8 -6
View File
@@ -16,24 +16,26 @@ export async function updateReadingProgress(
return; return;
} }
const payload: any = { const payload: ReadingProgress = {
current_page: progress.current_page, current_page: progress.current_page,
total_pages: progress.total_pages, total_pages: progress.total_pages,
epubcfi: progress.epubcfi || "", // ADD THIS cfi: progress.cfi || "",
percentage: extended?.percentage || 0, // ADD THIS percentage: extended?.percentage || 0,
last_read_at: new Date().toISOString(),
}; };
const response = await apiPut( const response = await apiPut(
`/media-items/${mediaItemId}/progress`, `/media-items/${mediaItemId}/progress`,
payload, payload,
); );
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const clonedResponse = response.clone();
console.warn("Progress update failed:", response.status, errorText); const errorText = await clonedResponse.text();
console.warn("Progress update failed:", clonedResponse.status, errorText);
return; return;
} }
} }
export function getChapterNavigation(chapters: any[]) { export function getChapterNavigation(chapters: Chapter[]) {
return { return {
getNextChapter: (currentPage: number) => { getNextChapter: (currentPage: number) => {
for (let i = 0; i < chapters.length - 1; i++) { for (let i = 0; i < chapters.length - 1; i++) {
@@ -1,7 +1,7 @@
// Search within ebook content // Search within ebook content
// Feature Registration Pattern implementation // Feature Registration Pattern implementation
import type { ReaderContext } from "../core/reader-context"; import type { ReaderContext } from "../../../core/reader-context";
export function init(context: ReaderContext): void { export function init(context: ReaderContext): void {
let ebookData: any = null; let ebookData: any = null;
@@ -139,4 +139,5 @@ function extractSnippet(text: string, offset: number, length: number): string {
if (end < text.length) snippet = snippet + "..."; if (end < text.length) snippet = snippet + "...";
return snippet; return snippet;
} }
@@ -1,14 +1,15 @@
// Import types // Import types
import type { ReadingPosition, ReflowableBook } from "./types"; import type { ReadingPosition } from "./types";
import { import {
getPageContent, getPageContent,
findPageByCFI, findPageByCFI,
createPositionFromPage, createPositionFromPage,
} from "./page-calculator"; } from "./page-calculator";
import { UniversalReader } from "../../reader-shell";
// Navigate to specific page // Navigate to specific page
export function goToPage( export function goToPage(
book: ReflowableBook, book: UniversalReader,
targetPage: number, targetPage: number,
): { ): {
success: boolean; success: boolean;
@@ -30,7 +31,7 @@ export function goToPage(
} }
// Navigate to next page // Navigate to next page
export function nextPage(book: ReflowableBook): { export function nextPage(book: UniversalReader): {
success: boolean; success: boolean;
position: ReadingPosition; position: ReadingPosition;
content: string; content: string;
@@ -40,7 +41,7 @@ export function nextPage(book: ReflowableBook): {
} }
// Navigate to previous page // Navigate to previous page
export function previousPage(book: ReflowableBook): { export function previousPage(book: UniversalReader): {
success: boolean; success: boolean;
position: ReadingPosition; position: ReadingPosition;
content: string; content: string;
@@ -51,7 +52,7 @@ export function previousPage(book: ReflowableBook): {
// Jump to specific CFI // Jump to specific CFI
export function goToCFI( export function goToCFI(
book: ReflowableBook, book: UniversalReader,
cfi: string, cfi: string,
): { ): {
success: boolean; success: boolean;
@@ -78,23 +79,23 @@ function createDefaultPosition(): ReadingPosition {
} }
// Check if navigation is possible // Check if navigation is possible
export function canGoNext(book: ReflowableBook): boolean { export function canGoNext(book: UniversalReader): boolean {
return book.position.currentPage < (book.pagination?.totalPages || 1); return book.position.currentPage < (book.pagination?.totalPages || 1);
} }
// Check if previous navigation is possible // Check if previous navigation is possible
export function canGoPrevious(book: ReflowableBook): boolean { export function canGoPrevious(book: UniversalReader): boolean {
return book.position.currentPage > 1; return book.position.currentPage > 1;
} }
// Get progress percentage // Get progress percentage
export function getProgressPercentage(book: ReflowableBook): number { export function getProgressPercentage(book: UniversalReader): number {
return Math.round(book.position.progress * 100); return Math.round(book.position.progress * 100);
} }
// Update book position (after resize/recalculation) // Update book position (after resize/recalculation)
export function updatePosition( export function updatePosition(
book: ReflowableBook, book: UniversalReader,
newCFI?: string, newCFI?: string,
): ReadingPosition { ): ReadingPosition {
if (newCFI && book.pagination) { if (newCFI && book.pagination) {
@@ -1,4 +1,5 @@
// Import types // Import types
import { UniversalReader } from "../../reader-shell";
import type { import type {
SpineItem, SpineItem,
SpineInfo, SpineInfo,
@@ -6,7 +7,6 @@ import type {
PaginationData, PaginationData,
PaginationSettings, PaginationSettings,
ReadingPosition, ReadingPosition,
ReflowableBook,
} from "./types"; } from "./types";
// Constants for word count estimation (from Kavita) // Constants for word count estimation (from Kavita)
@@ -452,7 +452,7 @@ export function shouldRecalculate(
// Create position object from page number // Create position object from page number
export function createPositionFromPage( export function createPositionFromPage(
book: ReflowableBook, book: UniversalReader,
page: number, page: number,
): ReadingPosition { ): ReadingPosition {
if (!book.pagination) { if (!book.pagination) {
+82 -20
View File
@@ -1,51 +1,113 @@
// Import types and existing parsers // Import types and existing parsers
import type { ReflowableBook, SpineItem, TOCItem } from "./types"; import type { TOCItem } from "./types";
import { parseEPUB } from "../../parsers/epub-parsers"; import { parseEPUB } from "../../parsers/epub-parsers";
import { parseFB2 } from "../../parsers/fb2-parser"; import { parseFB2 } from "../../parsers/fb2-parser";
import { parseTXT } from "../../parsers/txt-parser"; import { parseTXT } from "../../parsers/txt-parser";
import { parseHTML } from "../../parsers/html-parser"; import { parseHTML } from "../../parsers/html-parser";
import { UniversalReader } from "../../reader-shell";
// Parse any reflowable format // Parse any reflowable format
export async function parseReflowable( export async function parseReflowable(
file: File, file: File,
format: "epub" | "fb2" | "txt" | "html", format: "epub" | "fb2" | "txt" | "html",
): Promise<ReflowableBook> { ): Promise<UniversalReader> {
switch (format) { switch (format) {
case "epub": case "epub": {
return await parseEPUB(file); const cif = await parseEPUB(file); // Get EbookCIF
case "fb2": return {
return await parseFB2(file); // Wrap in UniversalReader structure
case "txt": type: "ebook",
return await parseTXT(file); cif, // ← The EbookCIF goes here
case "html": currentSpineIndex: 0,
return await parseHTML(file); currentPage: 1,
pagination: null,
position: {
currentPage: 1,
spineIndex: 0,
localPageIndex: 0,
cfi: "",
progress: 0,
},
};
}
case "fb2": {
const cif = await parseFB2(file);
return {
type: "ebook",
cif,
currentSpineIndex: 0,
currentPage: 1,
pagination: null,
position: {
currentPage: 1,
spineIndex: 0,
localPageIndex: 0,
cfi: "",
progress: 0,
},
};
}
case "txt": {
const cif = await parseTXT(file);
return {
type: "ebook",
cif,
currentSpineIndex: 0,
currentPage: 1,
pagination: null,
position: {
currentPage: 1,
spineIndex: 0,
localPageIndex: 0,
cfi: "",
progress: 0,
},
};
}
case "html": {
const cif = await parseHTML(file);
return {
type: "ebook",
cif,
currentSpineIndex: 0,
currentPage: 1,
pagination: null,
position: {
currentPage: 1,
spineIndex: 0,
localPageIndex: 0,
cfi: "",
progress: 0,
},
};
}
default: default:
throw new Error(`Unsupported reflowable format: ${format}`); throw new Error(`Unsupported reflowable format: ${format}`);
} }
} }
// Validate parsed book data // Validate parsed book data
export function validateBook(book: ReflowableBook): boolean { export function validateBook(book: UniversalReader): boolean {
return book.spine.length > 0 && book.metadata.title !== ""; return book.cif.spine.length > 0 && book.cif.metadata.title !== "";
} }
// Get book title // Get book title
export function getBookTitle(book: ReflowableBook): string { export function getBookTitle(book: UniversalReader): string {
return book.metadata.title || "Untitled"; return book.cif.metadata.title || "Untitled";
} }
// Get book author // Get book author
export function getBookAuthor(book: ReflowableBook): string { export function getBookAuthor(book: UniversalReader): string {
return book.metadata.author || "Unknown"; return book.cif.metadata.author || "Unknown";
} }
// Get total spine count // Get total spine count
export function getSpineCount(book: ReflowableBook): number { export function getSpineCount(book: UniversalReader): number {
return book.spine.length; return book.cif.spine.length;
} }
// Get TOC as flat list // Get TOC as flat list
export function getFlatTOC(book: ReflowableBook): TOCItem[] { export function getFlatTOC(book: UniversalReader): TOCItem[] {
const flat: TOCItem[] = []; const flat: TOCItem[] = [];
function traverse(items: TOCItem[]) { function traverse(items: TOCItem[]) {
@@ -57,6 +119,6 @@ export function getFlatTOC(book: ReflowableBook): TOCItem[] {
} }
} }
traverse(book.toc); traverse(book.cif.toc);
return flat; return flat;
} }
@@ -1,12 +1,13 @@
// Import types // Import types
import type { ReflowableBook, ReadingPosition } from "./types"; import type { ReadingPosition } from "./types";
import { findPageByCFI, createPositionFromPage } from "./page-calculator"; import { findPageByCFI, createPositionFromPage } from "./page-calculator";
import { UniversalReader } from "../../reader-shell";
// Update current position // Update current position
export function updateCurrentPosition( export function updateCurrentPosition(
book: ReflowableBook, book: UniversalReader,
position: ReadingPosition, position: ReadingPosition,
): ReflowableBook { ): UniversalReader {
return { return {
...book, ...book,
position, position,
@@ -14,12 +15,12 @@ export function updateCurrentPosition(
} }
// Extract CFI from position // Extract CFI from position
export function getCurrentCFI(book: ReflowableBook): string { export function getCurrentCFI(book: UniversalReader): string {
return book.position.cfi; return book.position.cfi;
} }
// Calculate progress for display // Calculate progress for display
export function calculateProgress(book: ReflowableBook): { export function calculateProgress(book: UniversalReader): {
currentPage: number; currentPage: number;
totalPages: number; totalPages: number;
percentage: number; percentage: number;
@@ -33,7 +34,7 @@ export function calculateProgress(book: ReflowableBook): {
} }
// Get position for saving to database // Get position for saving to database
export function getPositionForSave(book: ReflowableBook): { export function getPositionForSave(book: UniversalReader): {
cfi: string; cfi: string;
progress: number; progress: number;
page: number; page: number;
@@ -47,7 +48,7 @@ export function getPositionForSave(book: ReflowableBook): {
// Restore position from database // Restore position from database
export function restorePosition( export function restorePosition(
book: ReflowableBook, book: UniversalReader,
savedCFI: string, savedCFI: string,
savedPage?: number, savedPage?: number,
): ReadingPosition { ): ReadingPosition {
+66 -39
View File
@@ -19,30 +19,31 @@ import type {
ReadingPosition, ReadingPosition,
} from "./formats/reflowable/types"; } from "./formats/reflowable/types";
import { updatePageDisplay, updateProgressBar } from "./ui/page-display"; import { updatePageDisplay, updateProgressBar } from "./ui/page-display";
import { PDFDocumentProxy } from "pdfjs-dist";
export interface UniversalReader { export interface UniversalReader {
type: "ebook"; type: "ebook";
cif: any; cif: EbookCIF;
currentSpineIndex: number; currentSpineIndex: number;
currentScrollPosition?: number;
currentPage: number; currentPage: number;
pageCalculationResult?: any;
pagination?: PaginationData; pagination?: PaginationData;
position?: ReadingPosition; position?: ReadingPosition;
} }
interface PDFReader { export interface PDFReader {
type: "pdf"; type: "pdf";
doc: any; doc: PDFDocumentProxy;
currentPage: number; currentPage: number;
} }
interface ComicReader { export interface ComicReader {
type: "comic"; type: "comic";
images: Blob[]; images: Blob[];
currentPage: number; currentPage: number;
} }
interface MangaReader { export interface MangaReader {
type: "manga"; type: "manga";
images: Blob[]; images: Blob[];
currentPage: number; currentPage: number;
@@ -182,7 +183,9 @@ async function initializeReader(): Promise<void> {
} }
} }
async function initializeEbookReader(metadata: any): Promise<UniversalReader> { async function initializeEbookReader(
metadata: ReaderMetadata,
): Promise<UniversalReader> {
const { parseEbook, requiresServerParsing } = const { parseEbook, requiresServerParsing } =
await import("./core/parser-manager"); await import("./core/parser-manager");
const needsServer = requiresServerParsing( const needsServer = requiresServerParsing(
@@ -191,7 +194,7 @@ async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
); );
let ebookFile: Blob; let ebookFile: Blob;
if (needsServer) { if (needsServer) {
const response = await fetch(`/readers/${metadata.media_item_id}/parse`, { const response = await fetch(`/readers/${metadata.id}/parse`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
@@ -214,15 +217,15 @@ async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
); );
console.log("EPUB Parse Result:", { console.log("EPUB Parse Result:", {
spineLength: cif.spine?.length, spineLength: cif.spine?.length,
spineItems: cif.spine?.map((s: any) => s.href), spineItems: cif.spine?.map((s: SpineItem) => s.href),
tocLength: cif.toc?.length, tocLength: cif.toc?.length,
keys: Object.keys(cif), keys: Object.keys(cif),
}); });
// ============================================================ // ============================================================
// Fetch saved progress from backend // Fetch saved progress from backend
// ============================================================ // ============================================================
const savedProgress = await getReadingProgress(metadata.media_item_id); const savedProgress = await getReadingProgress(metadata.id);
const savedCFI = savedProgress?.epubcfi; const savedCFI = savedProgress?.cfi;
const savedPage = savedProgress?.current_page; const savedPage = savedProgress?.current_page;
console.log("Saved progress:", { savedCFI, savedPage }); console.log("Saved progress:", { savedCFI, savedPage });
// ============================================================ // ============================================================
@@ -245,11 +248,10 @@ async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
lineHeight: userSettings.line_height, lineHeight: userSettings.line_height,
marginWidth: userSettings.margin_width, marginWidth: userSettings.margin_width,
}); });
const spineItems = cif.spine.map((s: any) => ({ const spineItems = cif.spine.map((s: SpineItem) => ({
id: s.id || s.href, id: s.id,
type: "html" as const, type: "html" as const,
content: s.href, content: s.content,
href: s.href,
})); }));
const pagination = await calculatePagination( const pagination = await calculatePagination(
spineItems, spineItems,
@@ -265,15 +267,27 @@ async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
// ============================================================ // ============================================================
const position: ReadingPosition = restorePosition( const position: ReadingPosition = restorePosition(
{ {
type: "epub", type: "ebook",
spine: spineItems, cif: {
resources: cif.resources, spine: spineItems,
toc: cif.toc || [], resources: cif.resources,
metadata: { toc: cif.toc || [],
title: cif.metadata.title || metadata.title, metadata: {
author: cif.metadata.author || metadata.author, title: cif.metadata.title || metadata.title,
identifier: metadata.id, author: cif.metadata.author || metadata.author,
identifier: metadata.id,
},
pagination,
position: {
currentPage: 1,
spineIndex: 0,
localPageIndex: 0,
cfi: "",
progress: 0,
},
}, },
currentSpineIndex: 0,
currentPage: 1,
pagination, pagination,
position: { position: {
currentPage: 1, currentPage: 1,
@@ -300,15 +314,26 @@ async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
const reflowableNav = await import("./formats/reflowable/navigation"); const reflowableNav = await import("./formats/reflowable/navigation");
const { success, content: pageContent } = reflowableNav.goToPage( const { success, content: pageContent } = reflowableNav.goToPage(
{ {
type: "epub", type: "ebook",
spine: spineItems, cif: {
resources: cif.resources, spine: spineItems,
toc: cif.toc || [], resources: cif.resources,
metadata: { toc: cif.toc || [],
title: cif.metadata.title || metadata.title, metadata: {
author: cif.metadata.author || metadata.author, title: cif.metadata.title || metadata.title,
identifier: metadata.id, author: cif.metadata.author || metadata.author,
language: cif.metadata.language || "en",
publisher: cif.metadata.publisher,
isbn: cif.metadata.isbn,
coverImage: cif.metadata.coverImage,
},
locations: cif.locations || {
totalCharacters: 0,
estimatedPages: 0,
},
}, },
currentSpineIndex: position.spineIndex,
currentPage: position.currentPage,
pagination, pagination,
position, position,
}, },
@@ -332,18 +357,24 @@ async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
}; };
} }
async function initializePDFReader(metadata: any): Promise<PDFReader> { async function initializePDFReader(
metadata: ReaderMetadata,
): Promise<PDFReader> {
const { initializePDFReader } = await import("./formats/pdf/pdfjs-wrapper"); const { initializePDFReader } = await import("./formats/pdf/pdfjs-wrapper");
return initializePDFReader(metadata); return initializePDFReader(metadata);
} }
async function initializeComicReader(metadata: any): Promise<ComicReader> { async function initializeComicReader(
metadata: ReaderMetadata,
): Promise<ComicReader> {
const { initializeComicReader } = const { initializeComicReader } =
await import("./formats/comic/image-parser"); await import("./formats/comic/image-parser");
return initializeComicReader(metadata); return initializeComicReader(metadata);
} }
async function initializeMangaReader(metadata: any): Promise<MangaReader> { async function initializeMangaReader(
metadata: ReaderMetadata,
): Promise<MangaReader> {
const { initializeMangaReader } = const { initializeMangaReader } =
await import("./formats/comic/image-parser"); await import("./formats/comic/image-parser");
return initializeMangaReader(metadata); return initializeMangaReader(metadata);
@@ -460,14 +491,10 @@ function handleResize(): void {
updatedBook.position.currentPage, updatedBook.position.currentPage,
); );
if (success) { if (success) {
const pageData =
newPagination.pageMap.get(
updatedBook.position.currentPage - 1,
) || null;
const { const {
renderPage, renderPage,
} = require("./formats/reflowable/content-renderer"); } = require("./formats/reflowable/content-renderer");
renderPage(container, pageContent, pageData); renderPage(container, pageContent);
updatePageDisplay( updatePageDisplay(
container, container,
updatedBook.position.currentPage, updatedBook.position.currentPage,
+1 -1
View File
@@ -79,7 +79,6 @@ interface ReaderMetadata {
format_group: "reflowable" | "fixed_layout" | "comic_archive" | "unkown"; format_group: "reflowable" | "fixed_layout" | "comic_archive" | "unkown";
library_type_name: "ebooks" | "comics" | "manga" | "pdf"; library_type_name: "ebooks" | "comics" | "manga" | "pdf";
mime_type: string; mime_type: string;
mime_type: string;
manga_type: "unknown" | "no" | "yes" | "yes_and_right_to_left"; manga_type: "unknown" | "no" | "yes" | "yes_and_right_to_left";
file_path: string; file_path: string;
chapter_metadata?: ChapterMetadata; chapter_metadata?: ChapterMetadata;
@@ -104,6 +103,7 @@ interface Chapter {
title: string; title: string;
start_page: number; start_page: number;
page_count: number; page_count: number;
spine_index?: number;
} }
interface PanelData { interface PanelData {