feat: nextbook on next page if last page
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user