diff --git a/web/src/api-explorer-docs.ts b/web/src/api-explorer-docs.ts index 54d541a..adab7ea 100644 --- a/web/src/api-explorer-docs.ts +++ b/web/src/api-explorer-docs.ts @@ -4,11 +4,7 @@ import { getToken } from "./storage"; let endpointPath = ""; let exampleResponse: unknown = null; -function initAPIExplorerDoc( - path: string, - request: string, - response: string, -): void { +function initAPIExplorerDoc(path: string, response: string): void { endpointPath = path; exampleResponse = JSON.parse(response); } diff --git a/web/src/api.ts b/web/src/api.ts index b3ea0af..cb8249a 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -91,19 +91,10 @@ function handleError(error: unknown, context: string): void { // ============================================================ // Reader API Functions // ============================================================ -interface ChapterMetadata { - chapters: Chapter[]; -} -interface Chapter { - id: string; - title: string; - start_page: number; - page_count: number; -} interface ReadingProgress { current_page: number; total_pages: number; - epubcfi?: string; + cfi?: string; percentage?: number; last_read_at: string; } @@ -147,8 +138,6 @@ export { getReadingProgress, // ADD THIS updateReadingProgress, ReadingProgress, - ChapterMetadata, - Chapter, }; Alpine.data("api", () => ({ diff --git a/web/src/queue.ts b/web/src/queue.ts index 13687d9..6266d59 100644 --- a/web/src/queue.ts +++ b/web/src/queue.ts @@ -155,7 +155,7 @@ function renderQueueItems(items: QueueItemResponse[]): void { .join(""); } -function showQueueItemModal(itemId: string): void { +function showQueueItemModal(): void { const modal = document.getElementById("queue-item-modal"); const detailsContainer = document.getElementById("queue-item-details"); diff --git a/web/src/reader/formats/comic/background-color.ts b/web/src/reader/formats/comic/background-color.ts index c2306f1..4bf912b 100644 --- a/web/src/reader/formats/comic/background-color.ts +++ b/web/src/reader/formats/comic/background-color.ts @@ -1,23 +1,29 @@ // Background color options for manga/comics // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { const state = createBackgroundColorState(); applyBackgroundColor(state.current); - context.events.on("background-color:set", (detail: { color: BackgroundColor; customColor?: string }) => { - setBackgroundColor(state, detail.color, detail.customColor); - }); + context.events.on( + "background-color:set", + (detail: { color: BackgroundColor; customColor?: string }) => { + setBackgroundColor(state, detail.color, detail.customColor); + }, + ); context.events.on("background-color:toggle", () => { toggleBackgroundColor(state); }); - context.events.on("ui:show-settings", (detail: { container: HTMLElement }) => { - renderBackgroundColorPicker(detail.container, state); - }); + context.events.on( + "ui:show-settings", + (detail: { container: HTMLElement }) => { + renderBackgroundColorPicker(detail.container, state); + }, + ); context.events.on("reader:unload", () => { const picker = document.querySelector(".background-color-picker"); @@ -43,7 +49,9 @@ const backgroundColors: Record = { function createBackgroundColorState( initial: BackgroundColor = "black", ): BackgroundColorState { - const saved = localStorage.getItem("reader-background-color") as BackgroundColor; + const saved = localStorage.getItem( + "reader-background-color", + ) as BackgroundColor; return { current: saved || initial, customColor: "#000000", @@ -67,7 +75,8 @@ function setBackgroundColor( state.current = color; state.customColor = customColor || state.customColor; - const bgColor = color === "custom" ? state.customColor : backgroundColors[color]; + const bgColor = + color === "custom" ? state.customColor : backgroundColors[color]; document.documentElement.style.setProperty("--reader-bg-color", bgColor); const viewer = document.querySelector(".reader-content") as HTMLElement; @@ -85,7 +94,9 @@ function setBackgroundColor( return state; } -function toggleBackgroundColor(state: BackgroundColorState): BackgroundColorState { +function toggleBackgroundColor( + state: BackgroundColorState, +): BackgroundColorState { const order: BackgroundColor[] = ["black", "white", "gray", "sepia"]; const currentIndex = order.indexOf(state.current); const nextIndex = (currentIndex + 1) % order.length; @@ -132,4 +143,5 @@ function updateBackgroundColorUI( buttons.forEach((btn, index) => { btn.classList.toggle("border-blue-500", colors[index] === state.current); }); -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/comic/chapter-markers.ts b/web/src/reader/formats/comic/chapter-markers.ts index 1132a7f..bfc4512 100644 --- a/web/src/reader/formats/comic/chapter-markers.ts +++ b/web/src/reader/formats/comic/chapter-markers.ts @@ -2,15 +2,18 @@ // Visual indicators for chapter boundaries // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import type { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { let state: ChapterMarkerState | null = null; - context.events.on("reader:loaded", (detail: { chapters: ChapterInfo[]; currentPage: number }) => { - state = createChapterMarkerState(detail.chapters, detail.currentPage); - renderChapterMarkers(context.elements.readerContent, state); - }); + context.events.on( + "reader:loaded", + (detail: { chapters: ChapterInfo[]; currentPage: number }) => { + state = createChapterMarkerState(detail.chapters, detail.currentPage); + renderChapterMarkers(context.elements.readerContent, state); + }, + ); context.events.on("page-changed", (detail: { page: number }) => { if (state) { @@ -24,11 +27,14 @@ export function init(context: ReaderContext): void { } }); - context.events.on("chapter-markers:navigate", (detail: { chapterNumber: number }) => { - if (state) { - scrollToChapter(state, detail.chapterNumber); - } - }); + context.events.on( + "chapter-markers:navigate", + (detail: { chapterNumber: number }) => { + if (state) { + scrollToChapter(state, detail.chapterNumber); + } + }, + ); context.events.on("reader:unload", () => { const markers = document.querySelector(".chapter-markers"); @@ -145,4 +151,5 @@ function scrollToChapter( }), ); } -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/comic/page-cache.ts b/web/src/reader/formats/comic/page-cache.ts index 1dece20..93d2d4f 100644 --- a/web/src/reader/formats/comic/page-cache.ts +++ b/web/src/reader/formats/comic/page-cache.ts @@ -2,7 +2,7 @@ // Shared by both comic and manga readers // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; import { detectPanels } from "./panel-detection.service"; export function init(context: ReaderContext): void { @@ -12,15 +12,18 @@ export function init(context: ReaderContext): void { state = createPageCache(detail.mediaItemId); }); - context.events.on("page-cache:get", async (detail: { pageNumber: number }) => { - if (state) { - const result = await getCachedPage(state, detail.pageNumber); - context.events.emit("page-cache:loaded", { - page: detail.pageNumber, - image: result.page, - }); - } - }); + context.events.on( + "page-cache:get", + async (detail: { pageNumber: number }) => { + if (state) { + const result = await getCachedPage(state, detail.pageNumber); + context.events.emit("page-cache:loaded", { + page: detail.pageNumber, + image: result.page, + }); + } + }, + ); context.events.on("page-cache:prefetch", (detail: { startPage: number }) => { if (state) { @@ -34,15 +37,18 @@ export function init(context: ReaderContext): void { } }); - context.events.on("page-cache:detected-panels", async (detail: { pageNumber: number }) => { - if (state) { - const panels = await detectPagePanels(state, detail.pageNumber); - context.events.emit("page-cache:panels-ready", { - pageNumber: detail.pageNumber, - panels, - }); - } - }); + context.events.on( + "page-cache:detected-panels", + async (detail: { pageNumber: number }) => { + if (state) { + const panels = await detectPagePanels(state, detail.pageNumber); + context.events.emit("page-cache:panels-ready", { + pageNumber: detail.pageNumber, + panels, + }); + } + }, + ); context.events.on("reader:unload", () => { if (state) { @@ -185,4 +191,5 @@ export async function detectPagePanels( state.panelData.set(pageNumber, result); return result.panels; -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/comic/page-order.ts b/web/src/reader/formats/comic/page-order.ts index 340637b..e6a021c 100644 --- a/web/src/reader/formats/comic/page-order.ts +++ b/web/src/reader/formats/comic/page-order.ts @@ -2,14 +2,17 @@ // Auto-detect Japanese vs Western reading order // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { let state: PageOrderState | null = null; - context.events.on("reader:loaded", (detail: { totalPages: number; pageNames: string[] }) => { - state = createPageOrderState(detail.totalPages, detail.pageNames); - }); + context.events.on( + "reader:loaded", + (detail: { totalPages: number; pageNames: string[] }) => { + state = createPageOrderState(detail.totalPages, detail.pageNames); + }, + ); context.events.on("page-order:set", (detail: { mode: PageOrderMode }) => { if (state) { @@ -24,19 +27,25 @@ export function init(context: ReaderContext): void { } }); - context.events.on("page-order:reorder", (detail: { pageNumbers: number[] }) => { - if (state) { - const reordered = reorderPages(state, detail.pageNumbers); - context.events.emit("page-order:reordered", { pages: reordered }); - } - }); + context.events.on( + "page-order:reorder", + (detail: { pageNumbers: number[] }) => { + if (state) { + const reordered = reorderPages(state, detail.pageNumbers); + context.events.emit("page-order:reordered", { pages: reordered }); + } + }, + ); - context.events.on("page-order:display-number", (detail: { actualPage: number }) => { - if (state) { - const displayPage = getDisplayPageNumber(state, detail.actualPage); - context.events.emit("page-order:display-page", { displayPage }); - } - }); + context.events.on( + "page-order:display-number", + (detail: { actualPage: number }) => { + if (state) { + const displayPage = getDisplayPageNumber(state, detail.actualPage); + context.events.emit("page-order:display-page", { displayPage }); + } + }, + ); } type PageOrderMode = "auto" | "japanese" | "western"; @@ -139,4 +148,5 @@ function getDisplayPageNumber( } return actualPage; -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/comic/page-scrubber.ts b/web/src/reader/formats/comic/page-scrubber.ts index 1ee94d2..5faf4bb 100644 --- a/web/src/reader/formats/comic/page-scrubber.ts +++ b/web/src/reader/formats/comic/page-scrubber.ts @@ -1,15 +1,26 @@ // Page slider/scrubber for quick navigation // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { let state: PageScrubberState | null = null; let scrubberElement: HTMLElement | null = null; - context.events.on("reader:loaded", (detail: { currentPage: number; totalPages: number; container: HTMLElement }) => { - state = createPageScrubber(detail.container, detail.currentPage, detail.totalPages); - }); + context.events.on( + "reader:loaded", + (detail: { + currentPage: number; + totalPages: number; + container: HTMLElement; + }) => { + state = createPageScrubber( + detail.container, + detail.currentPage, + detail.totalPages, + ); + }, + ); context.events.on("page-scrubber:show", () => { if (state) { @@ -117,4 +128,5 @@ function dispatchPageNavigationEvent(page: number): void { window.dispatchEvent( new CustomEvent("navigate-to-page", { detail: { page } }), ); -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/comic/panel-gap.ts b/web/src/reader/formats/comic/panel-gap.ts index 3e84e24..d91e538 100644 --- a/web/src/reader/formats/comic/panel-gap.ts +++ b/web/src/reader/formats/comic/panel-gap.ts @@ -1,7 +1,7 @@ // Adjustable panel gap controls // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { const state = createPanelGapState(); @@ -23,9 +23,12 @@ export function init(context: ReaderContext): void { togglePanelBorders(state); }); - context.events.on("ui:show-settings", (detail: { container: HTMLElement }) => { - renderPanelGapControls(detail.container, state); - }); + context.events.on( + "ui:show-settings", + (detail: { container: HTMLElement }) => { + renderPanelGapControls(detail.container, state); + }, + ); context.events.on("reader:unload", () => { const controls = document.querySelector(".panel-gap-controls"); @@ -70,11 +73,17 @@ function setPanelGap(state: PanelGapState, gap: number): PanelGapState { return state; } -function increasePanelGap(state: PanelGapState, amount: number = 2): PanelGapState { +function increasePanelGap( + state: PanelGapState, + amount: number = 2, +): PanelGapState { return setPanelGap(state, state.gapSize + amount); } -function decreasePanelGap(state: PanelGapState, amount: number = 2): PanelGapState { +function decreasePanelGap( + state: PanelGapState, + amount: number = 2, +): PanelGapState { return setPanelGap(state, state.gapSize - amount); } @@ -147,4 +156,5 @@ function updatePanelGapUI(container: HTMLElement, state: PanelGapState): void { if (bordersBtn) { bordersBtn.textContent = state.showBorders ? "▦" : "▢"; } -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/manga/reading-direction.ts b/web/src/reader/formats/manga/reading-direction.ts index 6aaea6b..7382256 100644 --- a/web/src/reader/formats/manga/reading-direction.ts +++ b/web/src/reader/formats/manga/reading-direction.ts @@ -1,7 +1,7 @@ // Detect reading direction from metadata or user preference // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { let state: ReadingDirectionState | null = null; @@ -9,21 +9,30 @@ export function init(context: ReaderContext): void { context.events.on("reader:loaded", async (detail: { metadata: any }) => { state = await detectReadingDirection(detail.metadata); const effectiveDirection = getEffectiveDirection(state); - context.events.emit("reading-direction:detected", { direction: effectiveDirection }); + context.events.emit("reading-direction:detected", { + direction: effectiveDirection, + }); }); - context.events.on("reading-direction:set", (detail: { direction: "auto" | "ltr" | "rtl" | "vertical" }) => { - if (state) { - state.direction = detail.direction; - const effectiveDirection = getEffectiveDirection(state); - context.events.emit("reading-direction:changed", { direction: effectiveDirection }); - } - }); + context.events.on( + "reading-direction:set", + (detail: { direction: "auto" | "ltr" | "rtl" | "vertical" }) => { + if (state) { + state.direction = detail.direction; + const effectiveDirection = getEffectiveDirection(state); + context.events.emit("reading-direction:changed", { + direction: effectiveDirection, + }); + } + }, + ); context.events.on("reading-direction:get", () => { if (state) { const effectiveDirection = getEffectiveDirection(state); - context.events.emit("reading-direction:current", { direction: effectiveDirection }); + context.events.emit("reading-direction:current", { + direction: effectiveDirection, + }); } }); @@ -37,7 +46,9 @@ export function init(context: ReaderContext): void { context.events.on("reading-direction:is-vertical", () => { if (state) { const isVertical = shouldUseVerticalScroll(state); - context.events.emit("reading-direction:is-vertical-result", { isVertical }); + context.events.emit("reading-direction:is-vertical-result", { + isVertical, + }); } }); } @@ -71,9 +82,7 @@ async function detectReadingDirection( }; } -function detectFromMetadata( - metadata: any, -): "ltr" | "rtl" | "vertical" { +function detectFromMetadata(metadata: any): "ltr" | "rtl" | "vertical" { const mangaType = (metadata as any).manga_type; if (mangaType === "yes_and_right_to_left" || mangaType === "yes") { return "rtl"; @@ -126,4 +135,5 @@ function shouldUseRTL(state: ReadingDirectionState): boolean { function shouldUseVerticalScroll(state: ReadingDirectionState): boolean { return getEffectiveDirection(state) === "vertical"; -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/manga/rtl-navigator.ts b/web/src/reader/formats/manga/rtl-navigator.ts index 9da25bd..88789da 100644 --- a/web/src/reader/formats/manga/rtl-navigator.ts +++ b/web/src/reader/formats/manga/rtl-navigator.ts @@ -1,17 +1,20 @@ // Right-to-left navigation for manga // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { let state: RTLNavigatorState | null = null; - context.events.on("reader:loaded", (detail: { totalPages: number; currentPage?: number }) => { - state = createRTLNavigator(detail.totalPages); - if (detail.currentPage) { - state.currentPage = detail.currentPage; - } - }); + context.events.on( + "reader:loaded", + (detail: { totalPages: number; currentPage?: number }) => { + state = createRTLNavigator(detail.totalPages); + if (detail.currentPage) { + state.currentPage = detail.currentPage; + } + }, + ); context.events.on("navigation:next-page", () => { if (state) { @@ -93,4 +96,5 @@ function getProgress(state: RTLNavigatorState): { function getReadingProgressPercentage(state: RTLNavigatorState): number { return (state.currentPage / state.totalPages) * 100; -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/manga/settings.ts b/web/src/reader/formats/manga/settings.ts index cbbd3b5..0741cb0 100644 --- a/web/src/reader/formats/manga/settings.ts +++ b/web/src/reader/formats/manga/settings.ts @@ -1,7 +1,7 @@ // Manga-specific settings integration // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { let currentSettings: MangaSettings | null = null; @@ -12,14 +12,17 @@ export function init(context: ReaderContext): void { context.events.emit("manga-settings:loaded", currentSettings); }); - context.events.on("manga-settings:update", async (detail: { settings: Partial }) => { - if (currentSettings) { - currentSettings = { ...currentSettings, ...detail.settings }; - await updateMangaSettings(detail.settings); - applyMangaSettings(currentSettings); - context.events.emit("manga-settings:changed", currentSettings); - } - }); + context.events.on( + "manga-settings:update", + async (detail: { settings: Partial }) => { + if (currentSettings) { + currentSettings = { ...currentSettings, ...detail.settings }; + await updateMangaSettings(detail.settings); + applyMangaSettings(currentSettings); + context.events.emit("manga-settings:changed", currentSettings); + } + }, + ); context.events.on("manga-settings:get", () => { if (currentSettings) { @@ -96,4 +99,5 @@ function applyMangaSettings(settings: MangaSettings): void { } document.documentElement.dataset.webtoonMode = String(settings.webtoonMode); -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/manga/vertical-scroll-mode.ts b/web/src/reader/formats/manga/vertical-scroll-mode.ts index d78632f..ea966d8 100644 --- a/web/src/reader/formats/manga/vertical-scroll-mode.ts +++ b/web/src/reader/formats/manga/vertical-scroll-mode.ts @@ -1,25 +1,41 @@ // Vertical scroll mode for webtoons/manhwa // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { let state: VerticalScrollState | null = null; - context.events.on("reader:loaded", (detail: { container: HTMLElement; mediaItemId: string; totalPages: number }) => { - state = createVerticalScroll(detail.container, detail.mediaItemId, detail.totalPages); - }); + context.events.on( + "reader:loaded", + (detail: { + container: HTMLElement; + mediaItemId: string; + totalPages: number; + }) => { + state = createVerticalScroll( + detail.container, + detail.mediaItemId, + detail.totalPages, + ); + }, + ); - context.events.on("vertical-scroll:load-page", async (detail: { pageNumber: number }) => { - if (state) { - await loadPage(state, detail.pageNumber); - } - }); + context.events.on( + "vertical-scroll:load-page", + async (detail: { pageNumber: number }) => { + if (state) { + await loadPage(state, detail.pageNumber); + } + }, + ); context.events.on("vertical-scroll:get-current", () => { if (state) { const currentPage = getCurrentPageFromScroll(state); - context.events.emit("vertical-scroll:current-page", { page: currentPage }); + context.events.emit("vertical-scroll:current-page", { + page: currentPage, + }); } }); @@ -180,4 +196,5 @@ function destroyVerticalScroll(state: VerticalScrollState): void { state.container.innerHTML = ""; state.loadedPages.clear(); state.loadingPages.clear(); -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/pdf/annotation-layer.ts b/web/src/reader/formats/pdf/annotation-layer.ts index 859f9ee..c19268d 100644 --- a/web/src/reader/formats/pdf/annotation-layer.ts +++ b/web/src/reader/formats/pdf/annotation-layer.ts @@ -1,25 +1,34 @@ // Annotation layer for rendering highlights and notes on PDFs // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { const highlights = new Map(); - context.events.on("pdf:highlights:render", (detail: { container: HTMLElement; highlights: any[] }) => { - clearPDFHighlights(detail.container); - for (const highlight of detail.highlights) { - renderSinglePDFHighlight(detail.container, highlight, highlights); - } - }); + context.events.on( + "pdf:highlights:render", + (detail: { container: HTMLElement; highlights: any[] }) => { + clearPDFHighlights(detail.container); + for (const highlight of detail.highlights) { + renderSinglePDFHighlight(detail.container, highlight, highlights); + } + }, + ); - context.events.on("pdf:highlights:clear", (detail: { container: HTMLElement }) => { - clearPDFHighlights(detail.container); - }); + context.events.on( + "pdf:highlights:clear", + (detail: { container: HTMLElement }) => { + clearPDFHighlights(detail.container); + }, + ); - context.events.on("pdf:highlight:remove", (detail: { highlightId: string }) => { - removePDFHighlight(detail.highlightId, highlights); - }); + context.events.on( + "pdf:highlight:remove", + (detail: { highlightId: string }) => { + removePDFHighlight(detail.highlightId, highlights); + }, + ); context.events.on("reader:unload", () => { highlights.forEach((element) => element.remove()); @@ -90,7 +99,9 @@ function parseColor(color: string): string { function showNotePopup(highlight: PDFHighlight): void { console.log("Show note for highlight:", highlight.id); - const event = new CustomEvent("pdf:note-show", { detail: { highlightId: highlight.id } }); + const event = new CustomEvent("pdf:note-show", { + detail: { highlightId: highlight.id }, + }); window.dispatchEvent(event); } @@ -99,10 +110,14 @@ export function clearPDFHighlights(container: HTMLElement): void { Array.from(highlights).forEach((element) => element.remove()); } -export function removePDFHighlight(highlightId: string, highlights: Map): void { +export function removePDFHighlight( + highlightId: string, + highlights: Map, +): void { const element = highlights.get(highlightId); if (element) { element.remove(); highlights.delete(highlightId); } -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/pdf/page-cache.ts b/web/src/reader/formats/pdf/page-cache.ts deleted file mode 100644 index 0d64e52..0000000 --- a/web/src/reader/formats/pdf/page-cache.ts +++ /dev/null @@ -1,148 +0,0 @@ -// 5-page ahead cache for PDF pages -// Pre-renders canvas and text layer for nearby pages - -import { PDFPageProxy, PageViewport } from "pdfjs-dist"; - -interface CachedPage { - pageNumber: number; - canvas: HTMLCanvasElement; - textLayer: HTMLElement; - viewport: PageViewport; - timestamp: number; -} - -// 5-page ahead cache for PDF pages -// Procedural implementation (no OOP) - -interface CachedPage { - pageNumber: number; - canvas: HTMLCanvasElement; - textLayer: HTMLElement; - viewport: PageViewport; - timestamp: number; -} - -interface PDFPageCacheState { - cache: Map; - maxCacheSize: number; - renderCallbacks: Map void>>; -} - -function createPDFPageCache(maxCacheSize: number = 5): PDFPageCacheState { - return { - cache: new Map(), - maxCacheSize, - renderCallbacks: new Map(), - }; -} - -async function getCachedPage( - state: PDFPageCacheState, - pageNumber: number, - renderFn: ( - pageNumber: number, - ) => Promise<{ - canvas: HTMLCanvasElement; - textLayer: HTMLElement; - viewport: PageViewport; - }>, -): Promise { - const cached = state.cache.get(pageNumber); - if (cached) { - cached.timestamp = Date.now(); - return { ...state, page: cached }; - } - - const { canvas, textLayer, viewport } = await renderFn(pageNumber); - - const cachedPage: CachedPage = { - pageNumber, - canvas, - textLayer, - viewport, - timestamp: Date.now(), - }; - - const newCache = new Map(state.cache); - newCache.set(pageNumber, cachedPage); - - const callbacks = state.renderCallbacks.get(pageNumber); - if (callbacks) { - callbacks.forEach((cb) => cb()); - const newCallbacks = new Map(state.renderCallbacks); - newCallbacks.delete(pageNumber); - return { - ...state, - cache: newCache, - renderCallbacks: newCallbacks, - page: cachedPage, - }; - } - - return { ...state, cache: newCache, page: cachedPage }; -} - -function preloadPages( - state: PDFPageCacheState, - currentPage: number, - totalPages: number, -): PDFPageCacheState { - for (let i = 1; i <= state.maxCacheSize; i++) { - const pageNumber = currentPage + i; - if (pageNumber <= totalPages && !state.cache.has(pageNumber)) { - triggerPreload(pageNumber); - } - } - - return state; -} - -function triggerPreload(pageNumber: number): void { - console.log("Preloading page:", pageNumber); -} - -function invalidatePage( - state: PDFPageCacheState, - pageNumber: number, -): PDFPageCacheState { - const cached = state.cache.get(pageNumber); - if (cached) { - cached.canvas.remove(); - cached.textLayer.remove(); - - const newCache = new Map(state.cache); - newCache.delete(pageNumber); - - return { ...state, cache: newCache }; - } - - return state; -} - -function clearPageCache(state: PDFPageCacheState): PDFPageCacheState { - state.cache.forEach((page) => { - page.canvas.remove(); - page.textLayer.remove(); - }); - - return { - ...state, - cache: new Map(), - }; -} - -function onPageRendered( - state: PDFPageCacheState, - pageNumber: number, - callback: () => void, -): PDFPageCacheState { - const newCallbacks = new Map(state.renderCallbacks); - - if (!newCallbacks.has(pageNumber)) { - newCallbacks.set(pageNumber, []); - } - - newCallbacks.get(pageNumber)!.push(callback); - - return { ...state, renderCallbacks: newCallbacks }; -} diff --git a/web/src/reader/formats/pdf/pdf-links.ts b/web/src/reader/formats/pdf/pdf-links.ts index 9099eb6..8397097 100644 --- a/web/src/reader/formats/pdf/pdf-links.ts +++ b/web/src/reader/formats/pdf/pdf-links.ts @@ -2,6 +2,8 @@ // External links open in new tab // Procedural implementation (no OOP) +import { PDFDocumentProxy } from "pdfjs-dist"; + interface PDFLink { url: string; pageNumber?: number; diff --git a/web/src/reader/formats/pdf/pdf-minimap.ts b/web/src/reader/formats/pdf/pdf-minimap.ts index 1bbcc41..639aca2 100644 --- a/web/src/reader/formats/pdf/pdf-minimap.ts +++ b/web/src/reader/formats/pdf/pdf-minimap.ts @@ -13,7 +13,7 @@ function createPDFMiniMap( container: HTMLElement, onPageNavigate: (pageNumber: number) => void, ): PDFMiniMapState { - const miniMap = createMiniMapElement(container); + const miniMap = createMiniMapElement(); container.appendChild(miniMap); return { @@ -25,7 +25,7 @@ function createPDFMiniMap( }; } -function createMiniMapElement(container: HTMLElement): HTMLElement { +function createMiniMapElement(): HTMLElement { const miniMap = document.createElement("div"); miniMap.className = "pdf-minimap"; miniMap.innerHTML = ` diff --git a/web/src/reader/formats/pdf/pdf-navigation.ts b/web/src/reader/formats/pdf/pdf-navigation.ts index 84b5b87..1dfc1da 100644 --- a/web/src/reader/formats/pdf/pdf-navigation.ts +++ b/web/src/reader/formats/pdf/pdf-navigation.ts @@ -1,22 +1,27 @@ // PDF navigation: page turning, zoom, fit modes // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { let navState: PDFNavigationState | null = null; - context.events.on("reader:loaded", (detail: { container: HTMLElement; totalPages: number }) => { - navState = { - currentPage: 1, - totalPages: detail.totalPages, - currentScale: 1.0, - fitMode: "fit-width", - scrollContainer: detail.container.querySelector(".pdf-scroll-container") || detail.container, - }; - setupPDFKeyboardNav(context, navState); - setupPDFScrollTracking(context, navState); - }); + context.events.on( + "reader:loaded", + (detail: { container: HTMLElement; totalPages: number }) => { + navState = { + currentPage: 1, + totalPages: detail.totalPages, + currentScale: 1.0, + fitMode: "fit-width", + scrollContainer: + detail.container.querySelector(".pdf-scroll-container") || + detail.container, + }; + setupPDFKeyboardNav(context, navState); + setupPDFScrollTracking(context, navState); + }, + ); context.events.on("pdf:navigate:to-page", (detail: { page: number }) => { if (navState) { @@ -54,11 +59,14 @@ export function init(context: ReaderContext): void { } }); - context.events.on("pdf:fit:set", (detail: { mode: "fit-width" | "fit-page" | "fit-height" | "none" }) => { - if (navState) { - setPDFFitMode(navState, detail.mode, context); - } - }); + context.events.on( + "pdf:fit:set", + (detail: { mode: "fit-width" | "fit-page" | "fit-height" | "none" }) => { + if (navState) { + setPDFFitMode(navState, detail.mode, context); + } + }, + ); context.events.on("reader:unload", () => { navState = null; @@ -75,7 +83,11 @@ interface PDFNavigationState { scrollContainer: HTMLElement | null; } -function goToPDFPage(state: PDFNavigationState, pageNumber: number, context: ReaderContext): void { +function goToPDFPage( + state: PDFNavigationState, + pageNumber: number, + context: ReaderContext, +): void { if (pageNumber < 1 || pageNumber > state.totalPages) return; state.currentPage = pageNumber; @@ -89,7 +101,10 @@ function nextPDFPage(state: PDFNavigationState, context: ReaderContext): void { } } -function previousPDFPage(state: PDFNavigationState, context: ReaderContext): void { +function previousPDFPage( + state: PDFNavigationState, + context: ReaderContext, +): void { if (state.currentPage > 1) { goToPDFPage(state, state.currentPage - 1, context); } @@ -106,14 +121,22 @@ function scrollToPDFPage(state: PDFNavigationState, pageNumber: number): void { } } -function setPDFZoom(state: PDFNavigationState, scale: number, context: ReaderContext): void { +function setPDFZoom( + state: PDFNavigationState, + scale: number, + context: ReaderContext, +): void { state.currentScale = scale; state.fitMode = "none"; updatePDFZoom(state); context.events.emit("pdf:zoom-changed", { scale }); } -function setPDFFitMode(state: PDFNavigationState, mode: PageFitMode, context: ReaderContext): void { +function setPDFFitMode( + state: PDFNavigationState, + mode: PageFitMode, + context: ReaderContext, +): void { state.fitMode = mode; updatePDFZoom(state); context.events.emit("pdf:fit-changed", { mode }); @@ -137,7 +160,10 @@ function updatePDFZoom(state: PDFNavigationState): void { window.dispatchEvent(event); } -function setupPDFKeyboardNav(context: ReaderContext, state: PDFNavigationState): void { +function setupPDFKeyboardNav( + context: ReaderContext, + state: PDFNavigationState, +): void { document.addEventListener("keydown", (e) => { if (e.key === "ArrowRight" || e.key === "ArrowDown") { nextPDFPage(state, context); @@ -151,7 +177,10 @@ function setupPDFKeyboardNav(context: ReaderContext, state: PDFNavigationState): }); } -function setupPDFScrollTracking(context: ReaderContext, state: PDFNavigationState): void { +function setupPDFScrollTracking( + context: ReaderContext, + state: PDFNavigationState, +): void { if (!state.scrollContainer) return; state.scrollContainer.addEventListener("scroll", () => { @@ -182,4 +211,5 @@ function getCurrentPDFPage(state: PDFNavigationState): number { } return state.currentPage; -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/pdf/pdf-outline.ts b/web/src/reader/formats/pdf/pdf-outline.ts index 90b6f50..3397c08 100644 --- a/web/src/reader/formats/pdf/pdf-outline.ts +++ b/web/src/reader/formats/pdf/pdf-outline.ts @@ -1,6 +1,8 @@ // PDF outline/TOC navigation // Procedural implementation (no OOP) +import { PDFDocumentProxy } from "pdfjs-dist"; + interface PDFOutlineNode { id: string; title: string; diff --git a/web/src/reader/formats/pdf/pdf-text-selection.ts b/web/src/reader/formats/pdf/pdf-text-selection.ts index 21f4337..222e25a 100644 --- a/web/src/reader/formats/pdf/pdf-text-selection.ts +++ b/web/src/reader/formats/pdf/pdf-text-selection.ts @@ -1,7 +1,7 @@ // PDF text selection - Uses backend API for highlight creation // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../core/reader-context"; export function init(context: ReaderContext): void { let currentMediaItemId: string | null = null; @@ -15,22 +15,32 @@ export function init(context: ReaderContext): void { context.events.emit("pdf:selection-current", selection); }); - context.events.on("pdf:highlight:create", async (detail: { selection: PDFTextSelection; color: string }) => { - if (currentMediaItemId) { - try { - const highlight = await createPDFHighlight(currentMediaItemId, detail.selection, detail.color); - context.events.emit("pdf:highlight-created", highlight); - } catch (error) { - console.error("Failed to create highlight:", error); + context.events.on( + "pdf:highlight:create", + async (detail: { selection: PDFTextSelection; color: string }) => { + if (currentMediaItemId) { + try { + const highlight = await createPDFHighlight( + currentMediaItemId, + detail.selection, + detail.color, + ); + context.events.emit("pdf:highlight-created", highlight); + } catch (error) { + console.error("Failed to create highlight:", error); + } } - } - }); + }, + ); - context.events.on("pdf:highlights:load", async (detail: { container: HTMLElement }) => { - if (currentMediaItemId) { - await loadAndRenderPDFHighlights(currentMediaItemId, detail.container); - } - }); + context.events.on( + "pdf:highlights:load", + async (detail: { container: HTMLElement }) => { + if (currentMediaItemId) { + await loadAndRenderPDFHighlights(currentMediaItemId, detail.container); + } + }, + ); context.events.on("reader:unload", () => { currentMediaItemId = null; @@ -52,8 +62,9 @@ export function getPDFTextSelection(): PDFTextSelection | null { if (!text) return null; - const pageElement = - range.commonAncestorContainer.parentElement?.closest?.("[data-page-number]") as HTMLElement; + const pageElement = range.commonAncestorContainer.parentElement?.closest?.( + "[data-page-number]", + ) as HTMLElement; const pageNumber = pageElement?.dataset.pageNumber ? parseInt(pageElement.dataset.pageNumber) : getCurrentPDFPage(); @@ -114,10 +125,7 @@ export async function loadAndRenderPDFHighlights( } } -function renderPDFHighlight( - container: HTMLElement, - highlight: any, -): void { +function renderPDFHighlight(container: HTMLElement, highlight: any): void { const overlay = document.createElement("div"); overlay.className = "pdf-highlight-annotation"; overlay.dataset.highlightId = highlight.id; @@ -150,5 +158,8 @@ function parseColor(color: string): string { function getCurrentPDFPage(): number { const pageElement = document.querySelector("[data-page-number]"); - return pageElement ? parseInt(pageElement.getAttribute("data-page-number") || "1") : 1; -} \ No newline at end of file + return pageElement + ? parseInt(pageElement.getAttribute("data-page-number") || "1") + : 1; +} + diff --git a/web/src/reader/formats/reflowable/ebook/dictionary-popup.ts b/web/src/reader/formats/reflowable/ebook/dictionary-popup.ts index 92c7944..82a8554 100644 --- a/web/src/reader/formats/reflowable/ebook/dictionary-popup.ts +++ b/web/src/reader/formats/reflowable/ebook/dictionary-popup.ts @@ -1,12 +1,15 @@ // Dictionary lookup popup for ebooks // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../../core/reader-context"; export function init(context: ReaderContext): void { - context.events.on("dictionary:lookup", (detail: { word: string; position: { x: number; y: number } }) => { - showDictionaryPopup(detail.word, detail.position); - }); + context.events.on( + "dictionary:lookup", + (detail: { word: string; position: { x: number; y: number } }) => { + showDictionaryPopup(detail.word, detail.position); + }, + ); context.events.on("reader:loaded", () => { handleTextSelection(); @@ -80,4 +83,5 @@ async function lookupWord(word: string): Promise { throw new Error(`Failed to lookup word: ${word}`); } return await response.json(); -} \ No newline at end of file +} + diff --git a/web/src/reader/formats/reflowable/ebook/font-loader.ts b/web/src/reader/formats/reflowable/ebook/font-loader.ts index 25691e1..08cd9c2 100644 --- a/web/src/reader/formats/reflowable/ebook/font-loader.ts +++ b/web/src/reader/formats/reflowable/ebook/font-loader.ts @@ -1,7 +1,7 @@ // Font loading with performance optimization // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import { ReaderContext } from "../../../core/reader-context"; export function init(context: ReaderContext): void { const userPreferredFont = localStorage.getItem("reader-font") || "literata"; @@ -93,4 +93,5 @@ function applyFontStack(stack: string): void { document.documentElement.style.setProperty("--reader-font-family", stack); } -export { READING_FONTS, preloadFonts, getFontStack }; \ No newline at end of file +export { READING_FONTS, preloadFonts, getFontStack }; + diff --git a/web/src/reader/formats/reflowable/ebook/typography-engine.ts b/web/src/reader/formats/reflowable/ebook/typography-engine.ts index 4b93120..2a7ef3d 100644 --- a/web/src/reader/formats/reflowable/ebook/typography-engine.ts +++ b/web/src/reader/formats/reflowable/ebook/typography-engine.ts @@ -1,41 +1,53 @@ // Typography engine for ebook rendering // Feature Registration Pattern implementation -import type { ReaderContext } from "../core/reader-context"; +import type { ReaderContext } from "../../../core/reader-context"; export function init(context: ReaderContext): void { let currentConfig: TypographyConfig | null = null; - context.events.on("reader:loaded", (detail: { container: HTMLElement; config?: Partial }) => { - currentConfig = { - readingFont: "literata", - fontSize: 18, - lineHeight: 1.6, - marginTop: 0, - marginBottom: 16, - marginLeft: 0, - marginRight: 0, - textAlign: "left", - textIndent: 0, - hyphenate: false, - ligatures: true, - fontSmoothing: "auto", - ...detail.config, - }; - applyTypography(detail.container, currentConfig); - }); - - context.events.on("typography:update", (detail: { container: HTMLElement; config: Partial }) => { - if (currentConfig) { - currentConfig = updateTypographyConfig(currentConfig, detail.config); + context.events.on( + "reader:loaded", + (detail: { + container: HTMLElement; + config?: Partial; + }) => { + currentConfig = { + readingFont: "literata", + fontSize: 18, + lineHeight: 1.6, + marginTop: 0, + marginBottom: 16, + marginLeft: 0, + marginRight: 0, + textAlign: "left", + textIndent: 0, + hyphenate: false, + ligatures: true, + fontSmoothing: "auto", + ...detail.config, + }; applyTypography(detail.container, currentConfig); - } - }); + }, + ); - context.events.on("typography:measure", (detail: { container: HTMLElement }) => { - const time = measureReadingTime(detail.container); - context.events.emit("typography:reading-time", { minutes: time }); - }); + context.events.on( + "typography:update", + (detail: { container: HTMLElement; config: Partial }) => { + if (currentConfig) { + currentConfig = updateTypographyConfig(currentConfig, detail.config); + applyTypography(detail.container, currentConfig); + } + }, + ); + + context.events.on( + "typography:measure", + (detail: { container: HTMLElement }) => { + const time = measureReadingTime(detail.container); + context.events.emit("typography:reading-time", { minutes: time }); + }, + ); } interface TypographyConfig { @@ -100,11 +112,11 @@ function applyTypography( function getFontStack(fontId: string): string { const fonts: Record = { - "literata": "Literata, serif", - "crimson": "Crimson Text, serif", + literata: "Literata, serif", + crimson: "Crimson Text, serif", "source-serif": "Source Serif 4, serif", "eb-garamond": "EB Garamond, serif", - "libertinus": "Libertinus Serif, serif", + libertinus: "Libertinus Serif, serif", "noto-serif": "Noto Serif, serif", "charis-sil": "Charis SIL, serif", "ibm-plex": "IBM Plex Serif, serif", @@ -159,4 +171,5 @@ function measureReadingTime( return Math.ceil(minutes); } -export { applyTypography, getFontStack }; \ No newline at end of file +export { applyTypography, getFontStack }; + diff --git a/web/src/reader/ui/navigator-panel.ts b/web/src/reader/ui/navigator-panel.ts index d3a5967..99f569c 100644 --- a/web/src/reader/ui/navigator-panel.ts +++ b/web/src/reader/ui/navigator-panel.ts @@ -90,7 +90,6 @@ function setupNavigatorDragHandler(state: NavigatorState): void { document.addEventListener("mousemove", (e) => { if (!state.isDragging || !state.contentImage) return; - const containerRect = state.container.getBoundingClientRect(); const imgRect = state.contentImage.getBoundingClientRect(); const relX = (e.clientX - imgRect.left) / imgRect.width; @@ -132,8 +131,6 @@ function updateNavigatorViewport(state: NavigatorState): void { function handleNavigatorPan(state: NavigatorState, x: number, y: number): void { if (!state.contentImage) return; - const imgRect = state.contentImage.getBoundingClientRect(); - const viewportX = x * state.container.offsetWidth; const viewportY = y * state.container.offsetHeight; diff --git a/web/src/reader/ui/panel-dock-system.ts b/web/src/reader/ui/panel-dock-system.ts index 12b5f3e..03fe2bd 100644 --- a/web/src/reader/ui/panel-dock-system.ts +++ b/web/src/reader/ui/panel-dock-system.ts @@ -36,7 +36,7 @@ export function init(readerContext: ReaderContext): void { const panelState = state.panels.get(detail.panelId); if (panelState) { panelState.visible = !panelState.visible; - updatePanelVisibility(context, detail.panelId, panelState.visible); + updatePanelVisibility(detail.panelId, panelState.visible); } }); @@ -44,7 +44,7 @@ export function init(readerContext: ReaderContext): void { const panelState = state.panels.get(detail.panelId); if (panelState) { panelState.collapsed = !panelState.collapsed; - updatePanelCollapsed(context, detail.panelId, panelState.collapsed); + updatePanelCollapsed(detail.panelId, panelState.collapsed); } }); @@ -116,22 +116,14 @@ function createPanel( context.elements.readerContent.appendChild(panel); } -function updatePanelVisibility( - context: ReaderContext, - panelId: string, - visible: boolean, -): void { +function updatePanelVisibility(panelId: string, visible: boolean): void { const panel = document.getElementById(`panel-${panelId}`); if (panel) { panel.classList.toggle("panel-hidden", !visible); } } -function updatePanelCollapsed( - context: ReaderContext, - panelId: string, - collapsed: boolean, -): void { +function updatePanelCollapsed(panelId: string, collapsed: boolean): void { const panel = document.getElementById(`panel-${panelId}`); if (panel) { panel.classList.toggle("panel-collapsed", collapsed); diff --git a/web/src/unlinked_books.ts b/web/src/unlinked_books.ts index 069446b..b3848c9 100644 --- a/web/src/unlinked_books.ts +++ b/web/src/unlinked_books.ts @@ -204,11 +204,7 @@ function searchBooksForLink(): void { }); } -function selectBookForLink( - mediaItemId: string, - title: string, - _coverPath: string, -): void { +function selectBookForLink(mediaItemId: string, _coverPath: string): void { selectedMediaItem = mediaItemId; const resultsContainer = document.getElementById("link-search-results"); if (!resultsContainer) return; @@ -243,7 +239,6 @@ function confirmManualLink(): void { if (!progressIdInput) return; - const progressId = progressIdInput.value; const confidence = parseFloat(confidenceInput?.value || "0"); const bookTitle = bookTitleInput?.value || ""; const sha256 = sha256Input?.value || ""; @@ -456,7 +451,6 @@ function setupEventDelegation(): void { } else if (action === "select-book") { selectBookForLink( card.dataset.mediaItemId || "", - card.dataset.title || "", card.dataset.cover || "", ); } else if (action === "select-match") {