diff --git a/web/src/reader/reader-shell.ts b/web/src/reader/reader-shell.ts index 0e7e3d3..2872e8d 100644 --- a/web/src/reader/reader-shell.ts +++ b/web/src/reader/reader-shell.ts @@ -1,12 +1,12 @@ import { Alpine } from "../alpine"; -import { getReaderMetadata } from "../api"; -import "./comic/panel-editor"; +import { getReaderMetadata, getReadingProgress } from "../api"; +import "./formats/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"; import { renderSpineItem } from "./core/reader-navigation"; -import { parseReflowable } from "./formats/reflowable/parser"; +import { loadSettings } from "./settings-manager"; import { calculatePagination, shouldRecalculate, @@ -14,8 +14,9 @@ import { import { restorePosition } from "./formats/reflowable/progress-tracker"; import { applyPaginatedStyles } from "./formats/reflowable/content-renderer"; import type { - ReflowableBook, PaginationSettings, + PaginationData, + ReadingPosition, } from "./formats/reflowable/types"; import { updatePageDisplay, updateProgressBar } from "./ui/page-display"; @@ -25,6 +26,8 @@ export interface UniversalReader { currentSpineIndex: number; currentPage: number; pageCalculationResult?: any; + pagination?: PaginationData; + position?: ReadingPosition; } interface PDFReader { @@ -59,31 +62,39 @@ 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)), + (ctx) => import("./ui/gestures").then((m) => m.init(ctx)), + (ctx) => import("./ui/keyboard-shortcuts").then((m) => m.init(ctx)), + (ctx) => import("./ui/panel-dock-system").then((m) => m.init(ctx)), + (ctx) => import("./ui/navigator-panel").then((m) => m.init(ctx)), + (ctx) => import("./ui/offline-manager").then((m) => m.init(ctx)), + (ctx) => import("./ui/reading-speed-tracker").then((m) => m.init(ctx)), + (ctx) => import("./ui/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)), + (ctx) => + import("./formats/comic/background-color").then((m) => m.init?.(ctx)), + (ctx) => import("./formats/comic/chapter-markers").then((m) => m.init?.(ctx)), + (ctx) => import("./formats/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)), + (ctx) => import("./formats/pdf/pdf-navigation").then((m) => m.init?.(ctx)), + (ctx) => + import("./formats/pdf/pdf-text-selection").then((m) => m.init?.(ctx)), + (ctx) => import("./formats/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)), + (ctx) => + import("./formats/reflowable/ebook/copy-handler").then((m) => + m.init?.(ctx), + ), + (ctx) => + import("./formats/reflowable/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)), + (ctx) => + import("./formats/manga/reading-direction").then((m) => m.init?.(ctx)), + (ctx) => + import("./formats/manga/vertical-scroll-mode").then((m) => m.init?.(ctx)), ]; async function initializeFeatures(context: ReaderContext): Promise { @@ -174,14 +185,11 @@ async function initializeReader(): Promise { 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", @@ -191,17 +199,14 @@ async function initializeEbookReader(metadata: any): Promise { 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, @@ -213,27 +218,134 @@ async function initializeEbookReader(metadata: any): Promise { tocLength: cif.toc?.length, keys: Object.keys(cif), }); - console.log("Spine items detail:", JSON.stringify(cif.spine)); + // ============================================================ + // Fetch saved progress from backend + // ============================================================ + const savedProgress = await getReadingProgress(metadata.media_item_id); + const savedCFI = savedProgress?.epubcfi; + const savedPage = savedProgress?.current_page; + console.log("Saved progress:", { savedCFI, savedPage }); + // ============================================================ + // Calculate pagination + // ============================================================ + const userSettings = await loadSettings().catch(() => ({ + font_size: 16, + line_height: 1.6, + margin_width: 20, + })); + const settings: PaginationSettings = { + fontSize: userSettings.font_size, + lineHeight: userSettings.line_height, + viewportWidth: window.innerWidth, + viewportHeight: window.innerHeight - 120, + wordsPerPage: 250, + }; + console.log("Using settings:", { + fontSize: userSettings.font_size, + lineHeight: userSettings.line_height, + marginWidth: userSettings.margin_width, + }); + const spineItems = cif.spine.map((s: any) => ({ + id: s.id || s.href, + type: "html" as const, + content: s.href, + href: s.href, + })); + const pagination = await calculatePagination( + spineItems, + cif.resources, + settings, + ); + console.log("Pagination calculated:", { + totalPages: pagination.totalPages, + spines: pagination.spines.length, + }); + // ============================================================ + // Restore saved position or start at page 1 + // ============================================================ + const position: ReadingPosition = restorePosition( + { + type: "epub", + spine: spineItems, + resources: cif.resources, + toc: cif.toc || [], + metadata: { + title: cif.metadata.title || metadata.title, + author: cif.metadata.author || metadata.author, + identifier: metadata.id, + }, + pagination, + position: { + currentPage: 1, + spineIndex: 0, + localPageIndex: 0, + cfi: "", + progress: 0, + }, + }, + savedCFI || "", + savedPage, + ); + console.log("Restored position:", position); + // ============================================================ + // Apply paginated styles to container + // ============================================================ + const container = document.getElementById("reader-content"); + if (container) { + applyPaginatedStyles(); + } + // ============================================================ + // Render the first page + // ============================================================ + const reflowableNav = await import("./formats/reflowable/navigation"); + const { success, content: pageContent } = reflowableNav.goToPage( + { + type: "epub", + spine: spineItems, + resources: cif.resources, + toc: cif.toc || [], + metadata: { + title: cif.metadata.title || metadata.title, + author: cif.metadata.author || metadata.author, + identifier: metadata.id, + }, + pagination, + position, + }, + position.currentPage, + ); + if (success && container) { + const { renderPage } = + await import("./formats/reflowable/content-renderer"); + renderPage(container, pageContent); + // Update UI + updatePageDisplay(container, position.currentPage, pagination.totalPages); + updateProgressBar(container, Math.round(position.progress * 100)); + } return { type: "ebook", cif, - currentSpineIndex: 0, - currentPage: 1, + currentSpineIndex: position.spineIndex, + currentPage: position.currentPage, + pagination, + position, }; } async function initializePDFReader(metadata: any): Promise { - const { initializePDFReader } = await import("./pdf/pdfjs-wrapper"); + const { initializePDFReader } = await import("./formats/pdf/pdfjs-wrapper"); return initializePDFReader(metadata); } async function initializeComicReader(metadata: any): Promise { - const { initializeComicReader } = await import("./comic/image-parser"); + const { initializeComicReader } = + await import("./formats/comic/image-parser"); return initializeComicReader(metadata); } async function initializeMangaReader(metadata: any): Promise { - const { initializeMangaReader } = await import("./comic/image-parser"); + const { initializeMangaReader } = + await import("./formats/comic/image-parser"); return initializeMangaReader(metadata); } @@ -242,6 +354,149 @@ function getFileExtension(filepath: string): string { return match ? `.${match[1]}` : ""; } +// ============================================================ +// Viewport Resize Handler +// ============================================================ +function handleResize(): void { + const state = getState(); + if (!state.currentReader || state.currentReader.type !== "ebook") return; + const book = state.currentReader as UniversalReader; + if (!book.pagination) return; + loadSettings() + .then((userSettings) => { + const newSettings: PaginationSettings = { + fontSize: userSettings.font_size, + lineHeight: userSettings.line_height, + viewportWidth: window.innerWidth, + viewportHeight: window.innerHeight - 120, + wordsPerPage: 250, + }; + if (shouldRecalculate(book.pagination, newSettings)) { + const currentCFI = book.position.cfi; + const currentPage = book.position.currentPage; + calculatePagination(book.cif.spine, book.cif.resources, newSettings) + .then((newPagination) => { + const updatedBook: UniversalReader = { + type: "ebook", + cif: book.cif, + currentSpineIndex: book.position.spineIndex, + currentPage: book.position.currentPage, + pagination: newPagination, + position: { + currentPage: currentPage, + spineIndex: book.position.spineIndex, + localPageIndex: book.position.localPageIndex, + cfi: currentCFI, + progress: currentPage / newPagination.totalPages, + }, + }; + setState({ currentReader: updatedBook }); + const container = document.getElementById("reader-content"); + if (container) { + const reflowableNav = require("./formats/reflowable/navigation"); + const { success, content: pageContent } = reflowableNav.goToPage( + updatedBook, + updatedBook.position.currentPage, + ); + if (success) { + const { + renderPage, + } = require("./formats/reflowable/content-renderer"); + renderPage(container, pageContent); + updatePageDisplay( + container, + updatedBook.position.currentPage, + newPagination.totalPages, + ); + updateProgressBar( + container, + Math.round(updatedBook.position.progress * 100), + ); + } + } + }) + .catch((error) => { + console.error("Pagination recalculation failed:", error); + }); + } + }) + .catch((error) => { + console.warn( + "Failed to load user settings for resize, using defaults:", + error, + ); + const newSettings: PaginationSettings = { + fontSize: 16, + lineHeight: 1.6, + viewportWidth: window.innerWidth, + viewportHeight: window.innerHeight - 120, + wordsPerPage: 250, + }; + if (shouldRecalculate(book.pagination, newSettings)) { + const currentCFI = book.position.cfi; + const currentPage = book.position.currentPage; + calculatePagination(book.cif.spine, book.cif.resources, newSettings) + .then((newPagination) => { + const updatedBook: UniversalReader = { + type: "ebook", + cif: book.cif, + currentSpineIndex: book.position.spineIndex, + currentPage: book.position.currentPage, + pagination: newPagination, + position: { + currentPage: currentPage, + spineIndex: book.position.spineIndex, + localPageIndex: book.position.localPageIndex, + cfi: currentCFI, + progress: currentPage / newPagination.totalPages, + }, + }; + setState({ currentReader: updatedBook }); + const container = document.getElementById("reader-content"); + if (container) { + const reflowableNav = require("./formats/reflowable/navigation"); + const { success, content: pageContent } = reflowableNav.goToPage( + updatedBook, + updatedBook.position.currentPage, + ); + if (success) { + const pageData = + newPagination.pageMap.get( + updatedBook.position.currentPage - 1, + ) || null; + const { + renderPage, + } = require("./formats/reflowable/content-renderer"); + renderPage(container, pageContent, pageData); + updatePageDisplay( + container, + updatedBook.position.currentPage, + newPagination.totalPages, + ); + updateProgressBar( + container, + Math.round(updatedBook.position.progress * 100), + ); + } + } + }) + .catch((error) => { + console.error("Pagination recalculation failed:", error); + }); + } + }); +} +function debounce( + func: (...args: any[]) => void, + wait: number, +): (...args: any[]) => void { + let timeout: any; + return function (this: any, ...args: any[]) { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; +} +window.addEventListener("resize", debounce(handleResize, 300)); // ============================================================ // Alpine.js Integration // ============================================================ @@ -268,11 +523,10 @@ Alpine.data("readerShell", () => ({ get totalPages() { const state = getState(); if (!state.currentReader || !state.readerMetadata) return 0; - if (state.currentReader.type === "ebook") { - // Use dynamic page calculation if available - if (state.currentReader.pageCalculationResult) { - return state.currentReader.pageCalculationResult.totalPages; + // Use pagination if available + if ((state.currentReader as UniversalReader).pagination) { + return (state.currentReader as UniversalReader).pagination!.totalPages; } return state.currentReader.cif.spine.length; } else if (state.currentReader.type === "pdf") {