Skip to content
Merged
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
35 changes: 35 additions & 0 deletions bun.lock

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"epubjs": "^0.3.93",
"flags": "^4.0.3",
"input-otp": "^1.4.2",
"jszip": "3.10.1",
Expand Down
93 changes: 89 additions & 4 deletions src/app/reader/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { use, useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { AttachFileDialog } from "@/components/library/attach-file-dialog";
import { ComicViewer } from "@/components/reader/comic-viewer";
import { EpubViewer } from "@/components/reader/epub-viewer";
import { NextIssueOverlay } from "@/components/reader/next-issue-overlay";
import { PageIndicator } from "@/components/reader/page-indicator";
import { PdfViewer } from "@/components/reader/pdf-viewer";
Expand All @@ -23,12 +24,14 @@ import {
getAllComics,
getBookmarks,
getComic,
getEpubFile,
getFileFromHandle,
getPagesForComic,
getRemotePages,
loadPagesFromHandle,
saveBookmark,
saveNote,
updateEpubProgress,
updateReadingProgress,
} from "@/lib/storage";
import type { Bookmark, Comic, RemotePage } from "@/lib/types";
Expand Down Expand Up @@ -57,6 +60,14 @@ export default function ReaderPage({
// Native PDF viewing state
const [pdfFile, setPdfFile] = useState<File | null>(null);
const [useNativePdf, setUseNativePdf] = useState(false);
// EPUB reflowable viewer state
const [epubFile, setEpubFile] = useState<Blob | null>(null);
const [useEpubViewer, setUseEpubViewer] = useState(false);
const [epubLocation, setEpubLocation] = useState<string | undefined>(
undefined,
);
const [epubChapter, setEpubChapter] = useState<string | undefined>(undefined);
const [readingFraction, setReadingFraction] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const router = useRouter();

Expand Down Expand Up @@ -301,6 +312,36 @@ export default function ReaderPage({
}

try {
// EPUB reflowable viewer: load the raw EPUB file for epubjs
if (comic.format === "epub") {
let blob: Blob | null = null;

// Try file handle first (desktop, avoids IndexedDB storage)
if (comic.fileHandle) {
try {
const f = await getFileFromHandle(comic.fileHandle);
if (f) blob = f;
} catch {
// fall through to IndexedDB
}
}

// Fall back to stored epub file
if (!blob) {
blob = await getEpubFile(comic.id);
}

if (blob) {
setEpubFile(blob);
setUseEpubViewer(true);
setEpubLocation(comic.epubCfi || undefined);
setIsLoading(false);
return;
}

// No EPUB file found — fall through to legacy rasterized pages
}

let pages: Blob[] | null = null;

// For PDFs with a file handle, try native PDF viewing first
Expand Down Expand Up @@ -379,6 +420,38 @@ export default function ReaderPage({
[comic],
);

// EPUB location change handler with debounced progress save
const epubProgressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
null,
);
const handleEpubLocationChange = useCallback(
(cfi: string, fraction: number, chapter?: string) => {
setReadingFraction(fraction);
setEpubChapter(chapter);

// Debounce progress save
if (epubProgressTimerRef.current) {
clearTimeout(epubProgressTimerRef.current);
}
epubProgressTimerRef.current = setTimeout(() => {
if (comic) {
const approxPage = Math.floor(fraction * (comic.totalPages || 1));
updateEpubProgress(comic.id, cfi, approxPage);
}
}, 1000);
},
[comic],
);

// Cleanup epub progress timer
useEffect(() => {
return () => {
if (epubProgressTimerRef.current) {
clearTimeout(epubProgressTimerRef.current);
}
};
}, []);

// biome-ignore lint/correctness/useExhaustiveDependencies: loadBookmarks is stable
const handleBookmark = useCallback(async () => {
if (!comic) return;
Expand Down Expand Up @@ -473,9 +546,11 @@ export default function ReaderPage({
const isRemote = comic.sourceType === "remote";
const hasContent = isRemote
? remotePageData.length > 0 || usingCachedPages
: useNativePdf
? pdfFile !== null
: comic.hasFile && comic.totalPages;
: useEpubViewer
? epubFile !== null
: useNativePdf
? pdfFile !== null
: comic.hasFile && comic.totalPages;

if (!hasContent) {
return (
Expand Down Expand Up @@ -553,7 +628,14 @@ export default function ReaderPage({
/>

<main className="h-full w-full" onClick={toggleControls}>
{useNativePdf && pdfFile ? (
{useEpubViewer && epubFile ? (
<EpubViewer
file={epubFile}
initialCfi={epubLocation}
onLocationChange={handleEpubLocationChange}
onReady={handleTotalPagesChange}
/>
) : useNativePdf && pdfFile ? (
<PdfViewer
file={pdfFile}
currentPage={currentPage}
Expand All @@ -579,6 +661,8 @@ export default function ReaderPage({
(settings.showPageNumbers ?? false) &&
settings.layoutMode !== "scrolling"
}
chapterTitle={useEpubViewer ? epubChapter : undefined}
fraction={useEpubViewer ? readingFraction : undefined}
/>

<ReaderMenu
Expand All @@ -594,6 +678,7 @@ export default function ReaderPage({
onRefreshBookmarks={loadBookmarks}
onDelete={handleDelete}
onComicUpdate={loadComic}
readingFraction={useEpubViewer ? readingFraction : undefined}
/>

<QuickNoteDialog
Expand Down
1 change: 1 addition & 0 deletions src/components/library/attach-file-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export function AttachFileDialog({
"application/zip": [".cbz", ".zip"],
"application/x-rar-compressed": [".cbr", ".rar"],
"application/pdf": [".pdf"],
"application/epub+zip": [".epub"],
},
},
],
Expand Down
Loading