From 4d5855590a8d8cf2f78d7f15d22bcd0df9c5b694 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:21:34 +0000 Subject: [PATCH 1/3] Initial plan From 50a54053ea58a6f981456788f1ac0e1e72dd972c Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:25:42 +0000 Subject: [PATCH 2/3] Enhance PDF and EPUB readers with better UI and progress tracking Co-authored-by: Programming2055 <186182155+Programming2055@users.noreply.github.com> Agent-Logs-Url: https://github.com/Programming2055/Booky/sessions/834d2b2c-8198-4a93-8916-08e109e5b07a --- src/components/EpubReader/EpubReader.css | 6 ++ src/components/EpubReader/EpubReader.tsx | 35 ++++++++++- src/components/PdfReader/PdfReader.css | 78 ++++++++++++++++++++++-- src/components/PdfReader/PdfReader.tsx | 44 ++++++++++--- 4 files changed, 148 insertions(+), 15 deletions(-) diff --git a/src/components/EpubReader/EpubReader.css b/src/components/EpubReader/EpubReader.css index 85548ca..c200af0 100644 --- a/src/components/EpubReader/EpubReader.css +++ b/src/components/EpubReader/EpubReader.css @@ -74,6 +74,12 @@ text-overflow: ellipsis; } +.epub-reading-progress { + font-size: 12px; + font-weight: 400; + margin-left: 6px; +} + .epub-reader-topbar-actions { display: flex; align-items: center; diff --git a/src/components/EpubReader/EpubReader.tsx b/src/components/EpubReader/EpubReader.tsx index 04dc58c..c379ce9 100644 --- a/src/components/EpubReader/EpubReader.tsx +++ b/src/components/EpubReader/EpubReader.tsx @@ -146,6 +146,7 @@ export function EpubReader({ const [toc, setToc] = useState([]); const [showTocPanel, setShowTocPanel] = useState(false); const [readerKey, setReaderKey] = useState(0); + const [readingPercentage, setReadingPercentage] = useState(0); const renditionRef = useRef(null); const settingsRef = useRef(null); @@ -191,13 +192,28 @@ export function EpubReader({ renditionRef.current = rendition; applyRenditionStyles(rendition); rendition.spread(spreadMode === 'auto' ? 'auto' : 'none'); + + // Generate locations for better progress tracking + const book = rendition.book as any; + if (book && !book.locations?.length) { + book.locations.generate(1600).then(() => { + // Update percentage after locations are generated + if (location && typeof location === 'string' && book.locations.percentageFromCfi) { + const percentage = Math.round(book.locations.percentageFromCfi(location) * 100); + setReadingPercentage(percentage); + } + }).catch(() => { + // Silent fail - locations not critical + }); + } + highlights.forEach(h => { try { rendition.annotations.highlight(h.cfi, {}, undefined, 'epub-hl', { fill: h.color, 'fill-opacity': '0.35', 'mix-blend-mode': 'multiply' }); } catch { /* skip */ } }); - }, [applyRenditionStyles, spreadMode, highlights]); + }, [applyRenditionStyles, spreadMode, highlights, location]); useEffect(() => { if (renditionRef.current) applyRenditionStyles(renditionRef.current); @@ -216,7 +232,17 @@ export function EpubReader({ setLocation(cfi); if (cfi && cfi !== lastSavedRef.current) { lastSavedRef.current = cfi; - saveReadingProgress(bookId, { cfi, percentage: 0 }); + // Calculate reading percentage + if (renditionRef.current) { + const book = renditionRef.current.book as any; + if (book?.locations?.percentageFromCfi) { + const percentage = Math.round(book.locations.percentageFromCfi(cfi) * 100); + setReadingPercentage(percentage); + saveReadingProgress(bookId, { cfi, percentage }); + } else { + saveReadingProgress(bookId, { cfi, percentage: 0 }); + } + } } }, [bookId, saveReadingProgress]); @@ -284,6 +310,11 @@ export function EpubReader({ {title} + {readingPercentage > 0 && ( + + ({readingPercentage}%) + + )}
diff --git a/src/components/PdfReader/PdfReader.css b/src/components/PdfReader/PdfReader.css index 2457744..af3752c 100644 --- a/src/components/PdfReader/PdfReader.css +++ b/src/components/PdfReader/PdfReader.css @@ -12,11 +12,42 @@ .pdf-reader-topbar { display: flex; align-items: center; - justify-content: flex-end; - height: 32px; - padding: 0 4px; + justify-content: space-between; + height: 40px; + padding: 0 12px; background: #38383d; flex-shrink: 0; + border-bottom: 1px solid #4a4a4f; +} + +.pdf-reader-info { + display: flex; + align-items: center; + gap: 10px; + color: #f9f9fa; + font-size: 14px; + flex: 1; + min-width: 0; +} + +.pdf-reader-info svg { + flex-shrink: 0; +} + +.pdf-title { + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 300px; +} + +.pdf-page-info { + color: #b1b1b3; + font-size: 13px; + white-space: nowrap; + margin-left: auto; + padding-left: 12px; } .pdf-viewer-iframe { @@ -29,16 +60,53 @@ display: flex; align-items: center; justify-content: center; - width: 28px; - height: 28px; + width: 32px; + height: 32px; border: none; border-radius: 4px; background: transparent; color: #f9f9fa; cursor: pointer; transition: background 0.15s; + flex-shrink: 0; } .pdf-close-btn:hover { background: #e74c3c; } + +.pdf-loading-indicator { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + color: #f9f9fa; + z-index: 10; +} + +.pdf-spinner { + width: 40px; + height: 40px; + border: 3px solid rgba(255, 255, 255, 0.1); + border-top-color: #4a9eff; + border-radius: 50%; + animation: pdf-spin 0.8s linear infinite; +} + +@keyframes pdf-spin { + to { transform: rotate(360deg); } +} + +@media (max-width: 768px) { + .pdf-title { + max-width: 150px; + } + + .pdf-page-info { + font-size: 12px; + } +} diff --git a/src/components/PdfReader/PdfReader.tsx b/src/components/PdfReader/PdfReader.tsx index 9887cbb..aaed067 100644 --- a/src/components/PdfReader/PdfReader.tsx +++ b/src/components/PdfReader/PdfReader.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useCallback } from 'react'; +import { useEffect, useRef, useCallback, useState } from 'react'; import { useApp } from '../../context'; import './PdfReader.css'; @@ -13,16 +13,23 @@ interface PdfReaderProps { export function PdfReader({ fileUrl, bookId, + fileName, initialPage = 1, onClose, }: PdfReaderProps) { - const { saveReadingProgress } = useApp(); + const { saveReadingProgress, state } = useApp(); const iframeRef = useRef(null); const progressInterval = useRef>(undefined); + const [currentPage, setCurrentPage] = useState(initialPage); + const [totalPages, setTotalPages] = useState(0); + const [isLoading, setIsLoading] = useState(true); const basePath = import.meta.env.BASE_URL || '/'; const viewerUrl = `${basePath}pdfjs/web/viewer.html?file=${encodeURIComponent(fileUrl)}#page=${initialPage}`; + const bookFileName = fileName || state.books.find(b => b.id === bookId)?.fileName || 'document.pdf'; + const title = bookFileName.replace(/\.[^/.]+$/, ''); + // Poll the iframe's pdf.js viewer for page info to save reading progress const startProgressTracking = useCallback(() => { if (progressInterval.current) clearInterval(progressInterval.current); @@ -33,17 +40,20 @@ export function PdfReader({ if (!iframeWindow?.PDFViewerApplication?.pdfViewer) return; const viewer = iframeWindow.PDFViewerApplication.pdfViewer; - const currentPage = viewer.currentPageNumber; - const totalPages = iframeWindow.PDFViewerApplication.pagesCount; + const current = viewer.currentPageNumber; + const total = iframeWindow.PDFViewerApplication.pagesCount; - if (currentPage && totalPages) { - const percentage = Math.round((currentPage / totalPages) * 100); - saveReadingProgress(bookId, { currentPage, totalPages, percentage }); + if (current && total) { + setCurrentPage(current); + setTotalPages(total); + setIsLoading(false); + const percentage = Math.round((current / total) * 100); + saveReadingProgress(bookId, { currentPage: current, totalPages: total, percentage }); } } catch { // iframe not ready or cross-origin — ignore } - }, 2000); + }, 1500); }, [bookId, saveReadingProgress]); useEffect(() => { @@ -65,6 +75,18 @@ export function PdfReader({ return (
+
+ + + + + {title} + {!isLoading && totalPages > 0 && ( + + Page {currentPage} of {totalPages} ({Math.round((currentPage / totalPages) * 100)}%) + + )} +
+ {isLoading && ( +
+
+ Loading PDF... +
+ )}