feat: nextbook on next page if last page

This commit is contained in:
Julien Froidefond
2025-03-07 08:15:34 +01:00
parent 591a41149f
commit 66f467c66b
8 changed files with 66 additions and 10 deletions

View File

@@ -11,10 +11,10 @@ import { AppError } from "@/utils/errors";
async function BookPage({ params }: { params: { bookId: string } }) { async function BookPage({ params }: { params: { bookId: string } }) {
try { try {
const data: KomgaBookWithPages = await BookService.getBook(params.bookId); const data: KomgaBookWithPages = await BookService.getBook(params.bookId);
const nextBook = await BookService.getNextBook(params.bookId, data.book.seriesId);
return ( return (
<Suspense fallback={<BookSkeleton />}> <Suspense fallback={<BookSkeleton />}>
<ClientBookWrapper book={data.book} pages={data.pages} /> <ClientBookWrapper book={data.book} pages={data.pages} nextBook={nextBook} />
</Suspense> </Suspense>
); );
} catch (error) { } catch (error) {

View File

@@ -13,14 +13,16 @@ import { NavigationBar } from "./components/NavigationBar";
import { ControlButtons } from "./components/ControlButtons"; import { ControlButtons } from "./components/ControlButtons";
import { ReaderContent } from "./components/ReaderContent"; import { ReaderContent } from "./components/ReaderContent";
import { useReadingDirection } from "./hooks/useReadingDirection"; import { useReadingDirection } from "./hooks/useReadingDirection";
import { useTranslate } from "@/hooks/useTranslate";
export function BookReader({ book, pages, onClose }: BookReaderProps) { export function BookReader({ book, pages, onClose, nextBook }: BookReaderProps) {
const [isDoublePage, setIsDoublePage] = useState(false); const [isDoublePage, setIsDoublePage] = useState(false);
const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
const readerRef = useRef<HTMLDivElement>(null); const readerRef = useRef<HTMLDivElement>(null);
const isLandscape = useOrientation(); const isLandscape = useOrientation();
const { direction, toggleDirection, isRTL } = useReadingDirection(); const { direction, toggleDirection, isRTL } = useReadingDirection();
const { isFullscreen, toggleFullscreen } = useFullscreen(); const { isFullscreen, toggleFullscreen } = useFullscreen();
const { t } = useTranslate();
const { const {
currentPage, currentPage,
@@ -35,12 +37,14 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
zoomLevel, zoomLevel,
panPosition, panPosition,
handleDoubleClick, handleDoubleClick,
showEndMessage,
} = usePageNavigation({ } = usePageNavigation({
book, book,
pages, pages,
isDoublePage, isDoublePage,
onClose, onClose,
direction, direction,
nextBook,
}); });
const { preloadPage, getPageUrl, cleanCache } = usePageCache({ const { preloadPage, getPageUrl, cleanCache } = usePageCache({
@@ -92,6 +96,21 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
onClick={() => setShowControls(!showControls)} onClick={() => setShowControls(!showControls)}
> >
<div className="relative h-full w-full flex items-center justify-center"> <div className="relative h-full w-full flex items-center justify-center">
{showEndMessage && (
<div className="absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-50">
<div className="bg-background border rounded-lg shadow-lg p-6 max-w-md text-center">
<h3 className="text-lg font-semibold mb-2">{t("reader.endOfSeries")}</h3>
<p className="text-muted-foreground mb-4">{t("reader.endOfSeriesMessage")}</p>
<button
onClick={() => onClose?.(currentPage)}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
{t("reader.backToSeries")}
</button>
</div>
</div>
)}
<ControlButtons <ControlButtons
showControls={showControls} showControls={showControls}
onToggleControls={() => setShowControls(!showControls)} onToggleControls={() => setShowControls(!showControls)}

View File

@@ -8,9 +8,10 @@ import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.serv
interface ClientBookWrapperProps { interface ClientBookWrapperProps {
book: KomgaBook; book: KomgaBook;
pages: number[]; pages: number[];
nextBook: KomgaBook | null;
} }
export function ClientBookWrapper({ book, pages }: ClientBookWrapperProps) { export function ClientBookWrapper({ book, pages, nextBook }: ClientBookWrapperProps) {
const router = useRouter(); const router = useRouter();
const handleCloseReader = (currentPage: number) => { const handleCloseReader = (currentPage: number) => {
@@ -18,8 +19,9 @@ export function ClientBookWrapper({ book, pages }: ClientBookWrapperProps) {
method: "POST", method: "POST",
}); });
ClientOfflineBookService.setCurrentPage(book, currentPage); ClientOfflineBookService.setCurrentPage(book, currentPage);
router.back(); router.push(`/series/${book.seriesId}`);
//router.back();
}; };
return <BookReader book={book} pages={pages} onClose={handleCloseReader} />; return <BookReader book={book} pages={pages} onClose={handleCloseReader} nextBook={nextBook} />;
} }

View File

@@ -1,6 +1,7 @@
import { useState, useCallback, useEffect, useRef } from "react"; import { useState, useCallback, useEffect, useRef } from "react";
import type { KomgaBook } from "@/types/komga"; import type { KomgaBook } from "@/types/komga";
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service"; import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
import { useRouter } from "next/navigation";
interface UsePageNavigationProps { interface UsePageNavigationProps {
book: KomgaBook; book: KomgaBook;
@@ -8,6 +9,7 @@ interface UsePageNavigationProps {
isDoublePage: boolean; isDoublePage: boolean;
onClose?: (currentPage: number) => void; onClose?: (currentPage: number) => void;
direction: "ltr" | "rtl"; direction: "ltr" | "rtl";
nextBook?: KomgaBook | null;
} }
export const usePageNavigation = ({ export const usePageNavigation = ({
@@ -16,13 +18,16 @@ export const usePageNavigation = ({
isDoublePage, isDoublePage,
onClose, onClose,
direction, direction,
nextBook,
}: UsePageNavigationProps) => { }: UsePageNavigationProps) => {
const router = useRouter();
const cPage = ClientOfflineBookService.getCurrentPage(book); const cPage = ClientOfflineBookService.getCurrentPage(book);
const [currentPage, setCurrentPage] = useState(cPage < 1 ? 1 : cPage); const [currentPage, setCurrentPage] = useState(cPage < 1 ? 1 : cPage);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [secondPageLoading, setSecondPageLoading] = useState(true); const [secondPageLoading, setSecondPageLoading] = useState(true);
const [zoomLevel, setZoomLevel] = useState(1); const [zoomLevel, setZoomLevel] = useState(1);
const [panPosition, setPanPosition] = useState({ x: 0, y: 0 }); const [panPosition, setPanPosition] = useState({ x: 0, y: 0 });
const [showEndMessage, setShowEndMessage] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null); const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const touchStartXRef = useRef<number | null>(null); const touchStartXRef = useRef<number | null>(null);
const touchStartYRef = useRef<number | null>(null); const touchStartYRef = useRef<number | null>(null);
@@ -107,13 +112,29 @@ export const usePageNavigation = ({
}, [currentPage, isDoublePage, navigateToPage, shouldShowDoublePage]); }, [currentPage, isDoublePage, navigateToPage, shouldShowDoublePage]);
const handleNextPage = useCallback(() => { const handleNextPage = useCallback(() => {
if (currentPage === pages.length) return; if (currentPage === pages.length) {
if (nextBook) {
router.push(`/books/${nextBook.id}`);
return;
} else {
setShowEndMessage(true);
return;
}
}
if (isDoublePage && shouldShowDoublePage(currentPage)) { if (isDoublePage && shouldShowDoublePage(currentPage)) {
navigateToPage(Math.min(pages.length, currentPage + 2)); navigateToPage(Math.min(pages.length, currentPage + 2));
} else { } else {
navigateToPage(Math.min(pages.length, currentPage + 1)); navigateToPage(Math.min(pages.length, currentPage + 1));
} }
}, [currentPage, isDoublePage, navigateToPage, pages.length, shouldShowDoublePage]); }, [
currentPage,
isDoublePage,
navigateToPage,
pages.length,
shouldShowDoublePage,
nextBook,
router,
]);
const calculateDistance = (touch1: Touch, touch2: Touch) => { const calculateDistance = (touch1: Touch, touch2: Touch) => {
const dx = touch2.clientX - touch1.clientX; const dx = touch2.clientX - touch1.clientX;
@@ -301,5 +322,6 @@ export const usePageNavigation = ({
zoomLevel, zoomLevel,
panPosition, panPosition,
handleDoubleClick, handleDoubleClick,
showEndMessage,
}; };
}; };

View File

@@ -13,6 +13,7 @@ export interface BookReaderProps {
book: KomgaBook; book: KomgaBook;
pages: number[]; pages: number[];
onClose?: (currentPage: number) => void; onClose?: (currentPage: number) => void;
nextBook?: KomgaBook | null;
} }
export interface ThumbnailProps { export interface ThumbnailProps {

View File

@@ -381,7 +381,10 @@
"close": "Close", "close": "Close",
"previousPage": "Previous page", "previousPage": "Previous page",
"nextPage": "Next page" "nextPage": "Next page"
} },
"endOfSeries": "End of series",
"endOfSeriesMessage": "You have finished all the books in this series!",
"backToSeries": "Back to series"
}, },
"debug": { "debug": {
"title": "DEBUG", "title": "DEBUG",

View File

@@ -379,7 +379,10 @@
"close": "Fermer", "close": "Fermer",
"previousPage": "Page précédente", "previousPage": "Page précédente",
"nextPage": "Page suivante" "nextPage": "Page suivante"
} },
"endOfSeries": "Fin de la série",
"endOfSeriesMessage": "Vous avez terminé tous les tomes de cette série !",
"backToSeries": "Retourner à la série"
}, },
"header": { "header": {
"toggleSidebar": "Afficher/masquer le menu latéral", "toggleSidebar": "Afficher/masquer le menu latéral",

View File

@@ -5,6 +5,7 @@ import { ImageService } from "./image.service";
import { PreferencesService } from "./preferences.service"; import { PreferencesService } from "./preferences.service";
import { ERROR_CODES } from "../../constants/errorCodes"; import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors"; import { AppError } from "../../utils/errors";
import { SeriesService } from "./series.service";
export class BookService extends BaseApiService { export class BookService extends BaseApiService {
static async getBook(bookId: string): Promise<KomgaBookWithPages> { static async getBook(bookId: string): Promise<KomgaBookWithPages> {
@@ -31,6 +32,11 @@ export class BookService extends BaseApiService {
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error); throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error);
} }
} }
public static async getNextBook(bookId: string, seriesId: string): Promise<KomgaBook | null> {
const books = await SeriesService.getAllSeriesBooks(seriesId);
const currentIndex = books.findIndex((book) => book.id === bookId);
return books[currentIndex + 1] || null;
}
static async updateReadProgress( static async updateReadProgress(
bookId: string, bookId: string,