fix(web,api): lecteur — worker pdf.js dédié, shell commun et préférences par livre
Régression worker PDF : le worker pdf.js est désormais instancié une seule fois via un port dédié (?worker&url) et reconfiguré à chaque montage, au lieu d'un workerSrc recalculé qui cassait le rendu. - ReaderShell : chrome commun aux lecteurs (toolbar, zones de tap, statut) et contrat ReaderControls pour EPUB/PDF/CBZ - préférences de lecture par livre (mode horizontal/vertical, fit) : table reader_preferences + migrations idempotentes, module API, DTO partagés, client web avec fallback localStorage hors-ligne Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,21 +1,31 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { ArrowLeft, RotateCcw, Save } from "lucide-react";
|
||||
import type { BookDto } from "@readabook/shared";
|
||||
import { api, getApiFallback } from "../api/client";
|
||||
import { ErrorRibbon, Meter } from "../components/ui";
|
||||
import { navigate } from "../router";
|
||||
import { CbzReader } from "../reader/CbzReader";
|
||||
import { EpubReader } from "../reader/EpubReader";
|
||||
import { parseCbrPageLocator, parseCbzPageLocator, parsePdfPageLocator, pdfPagePercent } from "../reader/locators";
|
||||
import { PdfReader } from "../reader/PdfReader";
|
||||
import { ReaderShell, type ReaderControls } from "../reader/ReaderShell";
|
||||
import { useReaderPreferences } from "../reader/useReaderPreferences";
|
||||
import { useReaderProgress } from "../reader/useReaderProgress";
|
||||
|
||||
const idleControls: ReaderControls = {
|
||||
canPrevious: false,
|
||||
canNext: false,
|
||||
positionLabel: "Chargement",
|
||||
onPrevious: () => undefined,
|
||||
onNext: () => undefined
|
||||
};
|
||||
|
||||
export function ReaderPage({ bookId }: { bookId: number }) {
|
||||
const [book, setBook] = useState<BookDto | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>();
|
||||
const [page, setPage] = useState(1);
|
||||
const [readerControls, setReaderControls] = useState<ReaderControls>(idleControls);
|
||||
const [controlsVisible, setControlsVisible] = useState(true);
|
||||
const { progress, saving, error: progressError, save } = useReaderProgress(bookId);
|
||||
const { preferences, setMode, error: preferencesError } = useReaderPreferences(bookId);
|
||||
|
||||
async function loadBook() {
|
||||
setLoading(true);
|
||||
@ -35,6 +45,11 @@ export function ReaderPage({ bookId }: { bookId: number }) {
|
||||
void loadBook();
|
||||
}, [bookId]);
|
||||
|
||||
useEffect(() => {
|
||||
setReaderControls(idleControls);
|
||||
setControlsVisible(true);
|
||||
}, [bookId]);
|
||||
|
||||
useEffect(() => {
|
||||
const nextPage = parsePdfPageLocator(progress?.locator) ?? parseCbzPageLocator(progress?.locator) ?? parseCbrPageLocator(progress?.locator);
|
||||
if (nextPage) setPage((current) => (current === nextPage ? current : nextPage));
|
||||
@ -59,27 +74,22 @@ export function ReaderPage({ bookId }: { bookId: number }) {
|
||||
[book?.format, save]
|
||||
);
|
||||
|
||||
const readerError = error ?? progressError ?? preferencesError;
|
||||
|
||||
return (
|
||||
<div className="reader-page">
|
||||
<header className="reader-topbar">
|
||||
<button className="ghost-button" onClick={() => navigate(backHref)}>
|
||||
<ArrowLeft size={17} />
|
||||
Fiche
|
||||
</button>
|
||||
<div>
|
||||
<strong>{book?.title ?? "Ouverture du lecteur"}</strong>
|
||||
<span>{loading ? "Chargement" : saving ? "Sauvegarde" : "Progression synchronisee"}</span>
|
||||
</div>
|
||||
{error ? (
|
||||
<button className="ghost-button icon-only" onClick={() => void loadBook()} aria-label="Reessayer">
|
||||
<RotateCcw size={18} />
|
||||
</button>
|
||||
) : (
|
||||
<Save size={18} />
|
||||
)}
|
||||
</header>
|
||||
<ErrorRibbon message={error ?? progressError} />
|
||||
<Meter value={progress?.percent ?? 0} />
|
||||
<ReaderShell
|
||||
title={book?.title ?? "Ouverture du lecteur"}
|
||||
status={loading ? "Chargement" : saving ? "Sauvegarde" : "Progression synchronisee"}
|
||||
backHref={backHref}
|
||||
progress={progress?.percent ?? 0}
|
||||
error={readerError}
|
||||
onRetry={error ? () => void loadBook() : undefined}
|
||||
mode={preferences.mode}
|
||||
onModeChange={setMode}
|
||||
controls={readerControls}
|
||||
controlsVisible={controlsVisible}
|
||||
onToggleControls={() => setControlsVisible((visible) => !visible)}
|
||||
>
|
||||
{!book ? (
|
||||
<div className="reader-fallback">
|
||||
<span>{error ?? "Chargement du livre."}</span>
|
||||
@ -88,12 +98,32 @@ export function ReaderPage({ bookId }: { bookId: number }) {
|
||||
</button>
|
||||
</div>
|
||||
) : book.format === "pdf" ? (
|
||||
<PdfReader url={fileUrl} page={page} backHref={backHref} onPageCommit={savePdfPage} />
|
||||
<PdfReader
|
||||
url={fileUrl}
|
||||
page={page}
|
||||
backHref={backHref}
|
||||
mode={preferences.mode}
|
||||
onPageCommit={savePdfPage}
|
||||
onControlsChange={setReaderControls}
|
||||
/>
|
||||
) : book.format === "cbz" || book.format === "cbr" ? (
|
||||
<CbzReader bookId={book.id} page={page} onPageCommit={saveComicPage} />
|
||||
<CbzReader
|
||||
bookId={book.id}
|
||||
page={page}
|
||||
mode={preferences.mode}
|
||||
onPageCommit={saveComicPage}
|
||||
onControlsChange={setReaderControls}
|
||||
/>
|
||||
) : (
|
||||
<EpubReader url={fileUrl} locator={progress?.locator} backHref={backHref} onLocatorChange={saveEpubLocator} />
|
||||
<EpubReader
|
||||
url={fileUrl}
|
||||
locator={progress?.locator}
|
||||
backHref={backHref}
|
||||
mode={preferences.mode}
|
||||
onLocatorChange={saveEpubLocator}
|
||||
onControlsChange={setReaderControls}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ReaderShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user