feat(api,web): support CBZ — scan, métadonnées, lecteur d'albums
- api: cbz utilitaire commun, scanner/métadonnées (+ tests), books, schéma et migrations - web: CbzReader, ReaderPage/locators (+ tests), types et client API - shared: types formats Refs: #16
This commit is contained in:
70
apps/web/src/reader/CbzReader.tsx
Normal file
70
apps/web/src/reader/CbzReader.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../api/client";
|
||||
import type { CbzPagesDto } from "../api/types";
|
||||
|
||||
export function CbzReader({
|
||||
bookId,
|
||||
page,
|
||||
onPageCommit
|
||||
}: {
|
||||
bookId: number;
|
||||
page: number;
|
||||
onPageCommit: (page: number, pages: number) => void;
|
||||
}) {
|
||||
const [pages, setPages] = useState<CbzPagesDto | null>(null);
|
||||
const [error, setError] = useState<string>();
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
setError(undefined);
|
||||
api
|
||||
.cbzPages(bookId)
|
||||
.then((nextPages) => {
|
||||
if (!alive) return;
|
||||
setPages(nextPages);
|
||||
if (page > nextPages.pageCount) onPageCommit(nextPages.pageCount, nextPages.pageCount);
|
||||
})
|
||||
.catch(() => {
|
||||
if (alive) setError("Archive CBZ indisponible.");
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [bookId]);
|
||||
|
||||
const pageCount = pages?.pageCount ?? 1;
|
||||
const currentPage = Math.max(1, Math.min(page, pageCount));
|
||||
const currentName = pages?.pages.find((item) => item.page === currentPage)?.name;
|
||||
|
||||
function go(nextPage: number) {
|
||||
setImageError(false);
|
||||
onPageCommit(Math.max(1, Math.min(nextPage, pageCount)), pageCount);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cbz-reader">
|
||||
{error || imageError ? (
|
||||
<div className="reader-fallback">
|
||||
<span>{error ?? "Page CBZ indisponible."}</span>
|
||||
<button className="ghost-button" onClick={() => go(currentPage)}>
|
||||
Reessayer
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<img src={api.cbzPageUrl(bookId, currentPage)} alt={currentName ?? `Page ${currentPage}`} onError={() => setImageError(true)} />
|
||||
)}
|
||||
<div className="reader-stepper">
|
||||
<button className="ghost-button" onClick={() => go(currentPage - 1)}>
|
||||
Precedent
|
||||
</button>
|
||||
<span>
|
||||
{currentPage} / {pageCount}
|
||||
</span>
|
||||
<button className="ghost-button" onClick={() => go(currentPage + 1)}>
|
||||
Suivant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user