From b5ec6a8e978c681bb7e726d268307252a47d4a36 Mon Sep 17 00:00:00 2001 From: Git Agent Date: Mon, 24 Aug 2026 16:19:52 +0200 Subject: [PATCH] =?UTF-8?q?fix(web):=20lecteur=20CBZ/CBR=20mode=20vertical?= =?UTF-8?q?=20=E2=80=94=20crash=20React=20contenu=20par=20error=20boundary?= =?UTF-8?q?=20avec=20actions=20de=20r=C3=A9cup=C3=A9ration=20(r=C3=A9gress?= =?UTF-8?q?ion=20#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/ReaderPage.tsx | 154 ++++++++++++++++----- apps/web/src/pages/ReaderPageCrash.test.ts | 20 +++ 2 files changed, 137 insertions(+), 37 deletions(-) create mode 100644 apps/web/src/pages/ReaderPageCrash.test.ts diff --git a/apps/web/src/pages/ReaderPage.tsx b/apps/web/src/pages/ReaderPage.tsx index 311dc9e..93e0b51 100644 --- a/apps/web/src/pages/ReaderPage.tsx +++ b/apps/web/src/pages/ReaderPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { Component, useCallback, useEffect, useMemo, useState, type ErrorInfo, type ReactNode } from "react"; import type { BookDto } from "@readabook/shared"; import { api, getApiFallback } from "../api/client"; import { CbzReader } from "../reader/CbzReader"; @@ -10,6 +10,7 @@ import { clampReaderZoom, READER_ZOOM_DEFAULT, READER_ZOOM_STEP } from "../reade import { majorityVisiblePage, type ReaderMode } from "../reader/readerScroll"; import { useReaderPreferences } from "../reader/useReaderPreferences"; import { useReaderProgress } from "../reader/useReaderProgress"; +import { navigate } from "../router"; const idleControls: ReaderControls = { canPrevious: false, @@ -19,6 +20,44 @@ const idleControls: ReaderControls = { onNext: () => undefined }; +type ReaderCrashBoundaryProps = { + resetKey: string; + onError: (error: Error) => void; + fallbackRender: (error: Error, retry: () => void) => ReactNode; + children: ReactNode; +}; + +type ReaderCrashBoundaryState = { + error: Error | null; +}; + +class ReaderCrashBoundary extends Component { + state: ReaderCrashBoundaryState = { error: null }; + + static getDerivedStateFromError(error: Error) { + return { error }; + } + + componentDidCatch(error: Error, _errorInfo: ErrorInfo) { + this.props.onError(error); + } + + componentDidUpdate(previousProps: ReaderCrashBoundaryProps) { + if (previousProps.resetKey !== this.props.resetKey && this.state.error) { + this.setState({ error: null }); + } + } + + retry = () => { + this.setState({ error: null }); + }; + + render() { + if (this.state.error) return this.props.fallbackRender(this.state.error, this.retry); + return this.props.children; + } +} + export function ReaderPage({ bookId }: { bookId: number }) { const [book, setBook] = useState(null); const [loading, setLoading] = useState(true); @@ -114,6 +153,11 @@ export function ReaderPage({ bookId }: { bookId: number }) { }, [currentVisiblePage, page, saveReaderMode] ); + const returnToPagedMode = useCallback(() => { + setPage(currentVisiblePage() ?? page); + setMode("horizontal"); + saveReaderMode("horizontal"); + }, [currentVisiblePage, page, saveReaderMode]); const changeZoom = useCallback((nextZoom: number | ((currentZoom: number) => number)) => { const stage = document.querySelector(".reader-stage") as HTMLElement | null; const scrollRatioX = stage && stage.scrollWidth > stage.clientWidth ? (stage.scrollLeft + stage.clientWidth / 2) / stage.scrollWidth : 0.5; @@ -162,42 +206,78 @@ export function ReaderPage({ bookId }: { bookId: number }) { zoomControls={zoomControls} modeControls={modeControls} > - {!book ? ( -
- {error ?? "Chargement du livre."} - -
- ) : book.format === "pdf" ? ( - - ) : book.format === "cbz" || book.format === "cbr" ? ( - - ) : ( - - )} + setReaderControls(idleControls)} + fallbackRender={(crashError, retry) => ( +
+
+

Le lecteur a rencontré une erreur.

+

La page reste ouverte. Vous pouvez réessayer, revenir au mode page par page ou retourner à la fiche du livre.

+
+
+ + + +
+ {crashError.message && ( +
+ Détail technique +
{crashError.message}
+
+ )} +
+ )} + > + {!book ? ( +
+ {error ?? "Chargement du livre."} + +
+ ) : book.format === "pdf" ? ( + + ) : book.format === "cbz" || book.format === "cbr" ? ( + + ) : ( + + )} +
); } diff --git a/apps/web/src/pages/ReaderPageCrash.test.ts b/apps/web/src/pages/ReaderPageCrash.test.ts new file mode 100644 index 0000000..697e33b --- /dev/null +++ b/apps/web/src/pages/ReaderPageCrash.test.ts @@ -0,0 +1,20 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +describe("ReaderPage crash containment", () => { + it("keeps a reader shell or fallback mounted when the CBZ vertical reader crashes", () => { + const source = readFileSync(new URL("./ReaderPage.tsx", import.meta.url), "utf8"); + + expect(source).toContain(" { + const source = readFileSync(new URL("./ReaderPage.tsx", import.meta.url), "utf8"); + + expect(source).toContain("Le lecteur a rencontré une erreur."); + expect(source).toContain("Réessayer"); + expect(source).toContain("Revenir au mode page par page"); + expect(source).toContain("Retour à la fiche"); + }); +});