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 } }) {
try {
const data: KomgaBookWithPages = await BookService.getBook(params.bookId);
const nextBook = await BookService.getNextBook(params.bookId, data.book.seriesId);
return (
<Suspense fallback={<BookSkeleton />}>
<ClientBookWrapper book={data.book} pages={data.pages} />
<ClientBookWrapper book={data.book} pages={data.pages} nextBook={nextBook} />
</Suspense>
);
} catch (error) {

View File

@@ -13,14 +13,16 @@ import { NavigationBar } from "./components/NavigationBar";
import { ControlButtons } from "./components/ControlButtons";
import { ReaderContent } from "./components/ReaderContent";
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 [showControls, setShowControls] = useState(false);
const readerRef = useRef<HTMLDivElement>(null);
const isLandscape = useOrientation();
const { direction, toggleDirection, isRTL } = useReadingDirection();
const { isFullscreen, toggleFullscreen } = useFullscreen();
const { t } = useTranslate();
const {
currentPage,
@@ -35,12 +37,14 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
zoomLevel,
panPosition,
handleDoubleClick,
showEndMessage,
} = usePageNavigation({
book,
pages,
isDoublePage,
onClose,
direction,
nextBook,
});
const { preloadPage, getPageUrl, cleanCache } = usePageCache({
@@ -92,6 +96,21 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) {
onClick={() => setShowControls(!showControls)}
>
<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
showControls={showControls}
onToggleControls={() => setShowControls(!showControls)}

View File

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

View File

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

View File

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

View File

@@ -379,7 +379,10 @@
"close": "Fermer",
"previousPage": "Page précédente",
"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": {
"toggleSidebar": "Afficher/masquer le menu latéral",

View File

@@ -5,6 +5,7 @@ import { ImageService } from "./image.service";
import { PreferencesService } from "./preferences.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import { SeriesService } from "./series.service";
export class BookService extends BaseApiService {
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);
}
}
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(
bookId: string,