Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/node": "^24.12.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
Expand Down
6 changes: 6 additions & 0 deletions src/components/EpubReader/EpubReader.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
35 changes: 33 additions & 2 deletions src/components/EpubReader/EpubReader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export function EpubReader({
const [toc, setToc] = useState<NavItem[]>([]);
const [showTocPanel, setShowTocPanel] = useState(false);
const [readerKey, setReaderKey] = useState(0);
const [readingPercentage, setReadingPercentage] = useState(0);

const renditionRef = useRef<Rendition | null>(null);
const settingsRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -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);
Expand All @@ -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]);

Expand Down Expand Up @@ -284,6 +310,11 @@ export function EpubReader({
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z" />
</svg>
<span>{title}</span>
{readingPercentage > 0 && (
<span className="epub-reading-progress" style={{ color: t.headerFg, opacity: 0.7 }}>
({readingPercentage}%)
</span>
)}
</div>

<div className="epub-reader-topbar-actions">
Expand Down
78 changes: 73 additions & 5 deletions src/components/PdfReader/PdfReader.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
}
44 changes: 36 additions & 8 deletions src/components/PdfReader/PdfReader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback } from 'react';
import { useEffect, useRef, useCallback, useState } from 'react';
import { useApp } from '../../context';
import './PdfReader.css';

Expand All @@ -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<HTMLIFrameElement>(null);
const progressInterval = useRef<ReturnType<typeof setInterval>>(undefined);
const [currentPage, setCurrentPage] = useState<number>(initialPage);
const [totalPages, setTotalPages] = useState<number>(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);
Expand All @@ -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(() => {
Expand All @@ -65,13 +75,31 @@ export function PdfReader({
return (
<div className="pdf-reader-overlay">
<div className="pdf-reader-topbar">
<div className="pdf-reader-info">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
<span className="pdf-title" title={title}>{title}</span>
{!isLoading && totalPages > 0 && (
<span className="pdf-page-info">
Page {currentPage} of {totalPages} ({Math.round((currentPage / totalPages) * 100)}%)
</span>
)}
</div>
<button className="pdf-close-btn" onClick={onClose} title="Close (Esc)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
{isLoading && (
<div className="pdf-loading-indicator">
<div className="pdf-spinner" />
<span>Loading PDF...</span>
</div>
)}
<iframe
ref={iframeRef}
className="pdf-viewer-iframe"
Expand Down