Files
bookhoard/web/src/reader/core/reader-services.ts
T
john-okeefe 11039cfb89 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
2026-04-10 23:21:10 -04:00

75 lines
1.9 KiB
TypeScript

import { apiPut, ReadingProgress } from "../../api";
interface ExtendedProgress {
character?: number;
chapter?: number;
percentage?: number;
}
export async function updateReadingProgress(
mediaItemId: string,
progress: ReadingProgress,
extended?: ExtendedProgress,
): Promise<void> {
if (!mediaItemId || mediaItemId === "undefined") {
console.warn("Skipping progress update - no valid mediaItemId");
return;
}
const payload: ReadingProgress = {
current_page: progress.current_page,
total_pages: progress.total_pages,
cfi: progress.cfi || "",
percentage: extended?.percentage || 0,
last_read_at: new Date().toISOString(),
};
const response = await apiPut(
`/media-items/${mediaItemId}/progress`,
payload,
);
if (!response.ok) {
const clonedResponse = response.clone();
const errorText = await clonedResponse.text();
console.warn("Progress update failed:", clonedResponse.status, errorText);
return;
}
}
export function getChapterNavigation(chapters: Chapter[]) {
return {
getNextChapter: (currentPage: number) => {
for (let i = 0; i < chapters.length - 1; i++) {
const chapter = chapters[i];
const nextChapter = chapters[i + 1];
if (
currentPage >= chapter.start_page &&
currentPage < nextChapter.start_page
) {
return nextChapter.start_page;
}
}
return null;
},
getPreviousChapter: (currentPage: number) => {
for (let i = 1; i < chapters.length; i++) {
const chapter = chapters[i];
if (
currentPage >= chapter.start_page &&
currentPage < chapter.start_page + chapter.page_count
) {
return chapters[i - 1].start_page;
}
}
if (currentPage < chapters[0].start_page) {
return null;
}
return chapters[0].start_page;
},
};
}