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:
@@ -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";
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Vendored
+1
-1
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user