import { Alpine } from "../alpine"; import { getReaderMetadata } from "../api"; import "./comic/panel-editor"; import { createReaderContext, type ReaderContext } from "./core/reader-context"; import { getState, setState } from "./core/reader-state"; import { createNavigationAPI } from "./core/reader-navigation"; import { readerEvents } from "./core/reader-events"; interface UniversalReader { type: "ebook"; cif: any; currentSpineIndex: number; } interface PDFReader { type: "pdf"; doc: any; currentPage: number; } interface ComicReader { type: "comic"; images: Blob[]; currentPage: number; } interface MangaReader { type: "manga"; images: Blob[]; currentPage: number; readingDirection: "rtl" | "vertical"; } type CurrentReader = UniversalReader | PDFReader | ComicReader | MangaReader; let currentReader: CurrentReader | null = null; let readerMetadata: ReaderMetadata | null = null; // ============================================================ // Feature Registry // ============================================================ type FeatureInit = (context: ReaderContext) => void | Promise; const featureModules: FeatureInit[] = [ // Core features (ctx) => import("./features/gestures").then((m) => m.init(ctx)), (ctx) => import("./features/keyboard-shortcuts").then((m) => m.init(ctx)), (ctx) => import("./features/panel-dock-system").then((m) => m.init(ctx)), (ctx) => import("./features/navigator-panel").then((m) => m.init(ctx)), (ctx) => import("./features/offline-manager").then((m) => m.init(ctx)), (ctx) => import("./features/reading-speed-tracker").then((m) => m.init(ctx)), (ctx) => import("./features/progress-indicator").then((m) => m.init(ctx)), // Comic features (ctx) => import("./comic/background-color").then((m) => m.init?.(ctx)), (ctx) => import("./comic/chapter-markers").then((m) => m.init?.(ctx)), (ctx) => import("./comic/page-cache").then((m) => m.init?.(ctx)), // PDF features (ctx) => import("./pdf/pdf-navigation").then((m) => m.init?.(ctx)), (ctx) => import("./pdf/pdf-text-selection").then((m) => m.init?.(ctx)), (ctx) => import("./pdf/annotation-layer").then((m) => m.init?.(ctx)), // Ebook features (ctx) => import("./ebook/copy-handler").then((m) => m.init?.(ctx)), (ctx) => import("./ebook/font-loader").then((m) => m.init?.(ctx)), // Manga features (ctx) => import("./manga/reading-direction").then((m) => m.init?.(ctx)), (ctx) => import("./manga/vertical-scroll-mode").then((m) => m.init?.(ctx)), ]; async function initializeFeatures(context: ReaderContext): Promise { const results = await Promise.allSettled( featureModules.map((getInit) => getInit(context)), ); results.forEach((result, index) => { if (result.status === "rejected") { console.warn(`Feature ${index} failed to initialize:`, result.reason); } }); } // ============================================================ // Reader Initialization // ============================================================ async function initializeReader(): Promise { const mediaItemId = document.body.dataset.mediaItemId; console.log("Initializing reader for:", mediaItemId); if (!mediaItemId) { console.error("No mediaItemId on body"); return; } try { readerMetadata = await getReaderMetadata(mediaItemId); console.log("Got metadata:", readerMetadata); console.log("Library type:", readerMetadata.library_type_name); console.log("Mime type:", readerMetadata.mime_type); switch (readerMetadata.format_group) { case "reflowable": if ( readerMetadata.mime_type === "application/epub+zip" || readerMetadata.file_path.endsWith(".epub") ) { currentReader = await initializeEbookReader(readerMetadata); } break; case "fixed_layout": currentReader = await initializePDFReader(readerMetadata); break; case "comic_archive": // Check manga_type or library_type_name if ( readerMetadata.manga_type === "yes" || readerMetadata.manga_type === "yes_and_right_to_left" || readerMetadata.library_type_name === "manga" ) { currentReader = await initializeMangaReader(readerMetadata); } else { currentReader = await initializeComicReader(readerMetadata); } break; } if (!currentReader) return; setState({ currentReader, readerMetadata }); const context = createReaderContext( getState, setState, createNavigationAPI(), () => (context.render = () => { // Delegates to navigation module }), ); await initializeFeatures(context); readerEvents.emit("readerReady", currentReader); } catch (error) { console.error("Render initialization failed:", error); } } async function initializeEbookReader(metadata: any): Promise { const { parseEbook, requiresServerParsing } = await import("./core/parser-manager"); const needsServer = requiresServerParsing( metadata.mime_type, getFileExtension(metadata.file_path), ); let ebookFile: Blob; if (needsServer) { const response = await fetch(`/readers/${metadata.media_item_id}/parse`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mime_type: metadata.mime_type, file_path: metadata.file_path, }), }); if (!response.ok) { throw new Error(`Server parsing failed: ${response.statusText}`); } ebookFile = await response.blob(); } else { const response = await fetch(metadata.file_path); ebookFile = await response.blob(); } const cif = await parseEbook( ebookFile, metadata.mime_type, getFileExtension(metadata.file_path), ); return { type: "ebook", cif, currentSpineIndex: 0, }; } async function initializePDFReader(metadata: any): Promise { const { initializePDFReader } = await import("./pdf/pdfjs-wrapper"); return initializePDFReader(metadata); } async function initializeComicReader(metadata: any): Promise { const { initializeComicReader } = await import("./comic/image-parser"); return initializeComicReader(metadata); } async function initializeMangaReader(metadata: any): Promise { const { initializeMangaReader } = await import("./comic/image-parser"); return initializeMangaReader(metadata); } function getFileExtension(filepath: string): string { const match = filepath.match(/\.([^.]+)$/); return match ? `.${match[1]}` : ""; } // ============================================================ // Alpine.js Integration // ============================================================ Alpine.data("readerShell", () => ({ initReader() { initializeReader(); }, get currentPage() { const state = getState(); if (!state.currentReader) return 0; if (state.currentReader.type === "ebook") { return state.currentReader.currentSpineIndex + 1; } return state.currentReader.currentPage; }, get totalPages() { const state = getState(); if (!state.currentReader || !state.readerMetadata) return 0; if (state.currentReader.type === "ebook") { return state.currentReader.cif.spine.length; } else if (state.currentReader.type === "pdf") { return state.readerMetadata.total_pages || 0; } else { return (state.currentReader as any).images.length; } }, }));