Merge pull request 'feat/image-exclusion-list' (#1) from feat/image-exclusion-list into main

Reviewed-on: #1
This commit is contained in:
rov 2026-05-08 13:33:36 -03:00
commit b621ac64f2
8 changed files with 522 additions and 63 deletions

View File

@ -54,11 +54,16 @@ Api.interceptors.response.use(
}
const originalRequest = error.config as CustomAxiosRequestConfig;
const errorDTO = error.response?.data as ErrorResponseDTO;
if (error.response?.status === 401 && !originalRequest._retry) {
if (
error.response?.status === 401 &&
!originalRequest._retry &&
originalRequest.url !== "/authentication"
) {
const userDataStr = localStorage.getItem("userData");
if (!userDataStr) {
handleLogout();
handleLogout(errorDTO?.message);
return Promise.reject(error);
}
@ -119,7 +124,6 @@ Api.interceptors.response.use(
}
}
const errorDTO = error.response?.data as ErrorResponseDTO;
if (errorDTO?.message) {
toast.error(errorDTO.message);
}
@ -135,7 +139,9 @@ const handleLogout = (message?: string) => {
toast.error(message);
}
window.location.href = "/";
if (window.location.pathname !== "/login") {
window.location.href = "/login";
}
};
export const customInstance = <T>(

View File

@ -165,13 +165,18 @@ export interface DefaultResponseDTOMangaContentImagesDTO {
message?: string;
}
export interface MangaContentImageDTO {
uuid?: string;
objectKey?: string;
}
export interface MangaContentImagesDTO {
id: number;
/** @minLength 1 */
mangaTitle: string;
previousContentId?: number;
nextContentId?: number;
contentImageKeys: string[];
contentImages: MangaContentImageDTO[];
}
export interface DefaultResponseDTOMangaImportJobPageResponseDTO {
@ -221,9 +226,9 @@ export interface PageMangaImportJobDTO {
number?: number;
pageable?: PageableObject;
numberOfElements?: number;
sort?: SortObject;
first?: boolean;
last?: boolean;
sort?: SortObject;
empty?: boolean;
}
@ -306,9 +311,9 @@ export interface PageMangaListDTO {
number?: number;
pageable?: PageableObject;
numberOfElements?: number;
sort?: SortObject;
first?: boolean;
last?: boolean;
sort?: SortObject;
empty?: boolean;
}

View File

@ -0,0 +1,91 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* MangaMochi API
* OpenAPI spec version: 1.0
*/
import {
useMutation
} from '@tanstack/react-query';
import type {
MutationFunction,
QueryClient,
UseMutationOptions,
UseMutationResult
} from '@tanstack/react-query';
import type {
DefaultResponseDTOVoid
} from '../api.schemas';
import { customInstance } from '../../api';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* Gets the hash of the image with the given UUID and adds it to the exclusion list.
* @summary Exclude image by UUID
*/
export const excludeImage = (
uuid: string,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/management/image-exclusion/${encodeURIComponent(String(uuid))}`, method: 'POST', signal
},
options);
}
export const getExcludeImageMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof excludeImage>>, TError,{uuid: string}, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof excludeImage>>, TError,{uuid: string}, TContext> => {
const mutationKey = ['excludeImage'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof excludeImage>>, {uuid: string}> = (props) => {
const {uuid} = props ?? {};
return excludeImage(uuid,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type ExcludeImageMutationResult = NonNullable<Awaited<ReturnType<typeof excludeImage>>>
export type ExcludeImageMutationError = unknown
/**
* @summary Exclude image by UUID
*/
export const useExcludeImage = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof excludeImage>>, TError,{uuid: string}, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof excludeImage>>,
TError,
{uuid: string},
TContext
> => {
const mutationOptions = getExcludeImageMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}

View File

@ -0,0 +1,250 @@
import * as React from "react"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { ContextMenu as ContextMenuPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
)
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className
)}
{...props}
/>
)
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
)
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
className
)}
{...props}
/>
)
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium text-foreground data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View File

@ -2,37 +2,124 @@ import { buildImgproxyUrl, generateSrcSet } from "@/utils/imgproxy.ts";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { useState } from "react";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { useExcludeImage } from "@/api/generated/image-exclusion/image-exclusion";
interface ChapterImageProps {
imageKey: string;
pageNumber: number;
infiniteScroll?: boolean;
priority?: boolean;
imageKey: string;
uuid: string;
pageNumber: number;
infiniteScroll?: boolean;
priority?: boolean;
}
export const ChapterImage = ({ imageKey, pageNumber, infiniteScroll, priority }: ChapterImageProps) => {
const [isLoading, setIsLoading] = useState(true);
export const ChapterImage = ({
imageKey,
uuid,
pageNumber,
infiniteScroll,
priority,
}: ChapterImageProps) => {
const [isLoading, setIsLoading] = useState(true);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const excludeMutation = useExcludeImage();
return (
<div className={cn(
"relative w-full overflow-hidden",
isLoading && (infiniteScroll ? "min-h-[600px] aspect-[2/3]" : "aspect-[2/3]")
)}>
{isLoading && (
<Skeleton className="absolute inset-0 z-10 h-full w-full" />
)}
<img
src={buildImgproxyUrl(imageKey, 1200)}
srcSet={generateSrcSet(imageKey)}
sizes="(max-width: 896px) 100vw, 896px"
className={cn(
"w-full h-auto block m-0 p-0 border-none transition-opacity duration-300",
isLoading ? "opacity-0" : "opacity-100"
)}
alt={`Page ${pageNumber}`}
loading={priority ? "eager" : "lazy"}
onLoad={() => setIsLoading(false)}
/>
</div>
);
const handleCopyLink = () => {
const url = buildImgproxyUrl(imageKey, 1800);
navigator.clipboard.writeText(url).then(() => {
toast.success("Image link copied to clipboard");
});
};
const handleExclude = async () => {
try {
await excludeMutation.mutateAsync({ uuid });
toast.success("Image added to exclusion list");
setIsDialogOpen(false);
} catch (error) {
toast.error("Failed to exclude image");
console.error(error);
}
};
return (
<>
<ContextMenu>
<ContextMenuTrigger disabled={isLoading}>
<div
className={cn(
"relative w-full overflow-hidden",
isLoading && (infiniteScroll ? "min-h-[600px] aspect-[2/3]" : "aspect-[2/3]"),
)}
>
{isLoading && (
<Skeleton className="absolute inset-0 z-10 h-full w-full" />
)}
<img
src={buildImgproxyUrl(imageKey, 1200)}
srcSet={generateSrcSet(imageKey)}
sizes="(max-width: 896px) 100vw, 896px"
className={cn(
"w-full h-auto block m-0 p-0 border-none transition-opacity duration-300",
isLoading ? "opacity-0" : "opacity-100",
)}
alt={`Page ${pageNumber}`}
loading={priority ? "eager" : "lazy"}
onLoad={() => setIsLoading(false)}
/>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-64">
<ContextMenuItem onClick={handleCopyLink}>
Copy image link
</ContextMenuItem>
<ContextMenuItem
onClick={() => setIsDialogOpen(true)}
className="text-destructive focus:text-destructive"
>
Add image to exclusion list
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Exclude Image</DialogTitle>
<DialogDescription>
Are you sure you want to add this image to the exclusion list? This
will prevent it from appearing in the reader for all users.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleExclude}
disabled={excludeMutation.isPending}
>
{excludeMutation.isPending ? "Excluding..." : "Exclude Image"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View File

@ -3,6 +3,7 @@ import {
useGetProgress1,
useUpdateProgress,
} from "@/api/generated/reading-progress/reading-progress";
import { useAuth } from "@/contexts/AuthContext.tsx";
interface ReadingTrackerData {
chapterPage: { [chapterId: number]: number };
@ -10,6 +11,7 @@ interface ReadingTrackerData {
}
export const useReadingProgressSync = (mangaId: number, chapterId: number) => {
const { isAuthenticated } = useAuth();
const [currentPage, setCurrentPage] = useState<number | null>(() => {
const jsonString = localStorage.getItem("readingTrackerData");
if (jsonString) {
@ -29,7 +31,7 @@ export const useReadingProgressSync = (mangaId: number, chapterId: number) => {
const { data: progressData, isLoading: isLoadingProgress } = useGetProgress1(
mangaId,
chapterId,
{ query: { retry: false } },
{ query: { retry: false, enabled: isAuthenticated } },
);
const { mutate: updateProgress } = useUpdateProgress();
@ -82,6 +84,7 @@ export const useReadingProgressSync = (mangaId: number, chapterId: number) => {
if (syncTimerRef.current) clearTimeout(syncTimerRef.current);
const performSync = () => {
if (!isAuthenticated) return;
if (forceSync || page !== lastSyncedPage.current) {
updateProgress({
data: {

View File

@ -48,10 +48,15 @@ const Chapter = () => {
/** Conflict Resolution & Initial Sync */
useEffect(() => {
if (isLoadingProgress || hasInitialized) return;
if (isLoadingProgress || !data?.data || hasInitialized) return;
const localPage = savedPage;
const serverPage = serverProgress?.pageNumber;
const images = data.data.contentImages;
const maxPages = images.length || 1;
const localPage = savedPage ? Math.min(savedPage, maxPages) : null;
const serverPage = serverProgress?.pageNumber
? Math.min(serverProgress.pageNumber, maxPages)
: null;
if (localPage === null) {
// No local history for this chapter, use server or default to 1
@ -87,13 +92,13 @@ const Chapter = () => {
}
setHasInitialized(true);
}
}, [isLoadingProgress, savedPage, serverProgress, hasInitialized]);
}, [isLoadingProgress, data, savedPage, serverProgress, hasInitialized]);
/** Mark chapter as read when last page reached */
useEffect(() => {
if (!data || isLoading || !hasInitialized || isAutoScrolling) return;
if (currentPage === data.data?.contentImageKeys.length) {
if (currentPage === data.data?.contentImages.length) {
mutate({ mangaContentId: chapterNumber });
}
}, [data, mutate, currentPage, hasInitialized, isAutoScrolling]);
@ -151,7 +156,7 @@ const Chapter = () => {
const obs = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
setVisibleCount((count) =>
Math.min(count + 2, data?.data?.contentImageKeys.length ?? 0),
Math.min(count + 2, data?.data?.contentImages.length ?? 0),
);
}
});
@ -222,7 +227,7 @@ const Chapter = () => {
);
}
const images = data.data.contentImageKeys;
const images = data.data.contentImages;
const previousContentId = data.data.previousContentId;
const nextContentId = data.data.nextContentId;
@ -269,10 +274,10 @@ const Chapter = () => {
this chapter.
<br />
<span className="mt-2 block">
Local: <strong>Page {savedPage}</strong>
Local: <strong>Page {savedPage ? Math.min(savedPage, images.length) : 1}</strong>
</span>
<span className="block">
Server: <strong>Page {serverProgress?.pageNumber}</strong>
Server: <strong>Page {serverProgress?.pageNumber ? Math.min(serverProgress.pageNumber, images.length) : 1}</strong>
</span>
</DialogDescription>
</DialogHeader>
@ -281,11 +286,12 @@ const Chapter = () => {
variant="outline"
onClick={() => {
if (savedPage && savedPage > 1) {
setCurrentPage(savedPage);
setVisibleCount(savedPage);
setIsAutoScrolling(true);
const clamped = Math.min(savedPage, images.length);
setCurrentPage(clamped);
setVisibleCount(clamped);
setIsAutoScrolling(clamped > 1);
initialJumpDone.current = false;
saveLocalProgress(savedPage, true);
saveLocalProgress(clamped, true);
}
setHasInitialized(true);
setShowConflictDialog(false);
@ -297,9 +303,10 @@ const Chapter = () => {
onClick={() => {
const target = applyServerProgress();
if (target) {
setCurrentPage(target);
setVisibleCount(target);
if (target > 1) {
const clamped = Math.min(target, images.length);
setCurrentPage(clamped);
setVisibleCount(clamped);
if (clamped > 1) {
setIsAutoScrolling(true);
initialJumpDone.current = false;
}
@ -410,7 +417,7 @@ const Chapter = () => {
{/* ------------------------------------------------------------------ */}
{infiniteScroll ? (
<div className="flex flex-col space-y-0">
{images.slice(0, visibleCount).map((key, idx) => (
{images.slice(0, visibleCount).map((img, idx) => (
<div
key={idx}
data-page={idx + 1}
@ -422,7 +429,8 @@ const Chapter = () => {
}}
>
<ChapterImage
imageKey={key}
imageKey={img.objectKey || ""}
uuid={img.uuid || ""}
pageNumber={idx + 1}
infiniteScroll
/>
@ -433,7 +441,8 @@ const Chapter = () => {
{visibleCount < images.length && (
<div style={{ display: "none" }} aria-hidden="true">
<ChapterImage
imageKey={images[visibleCount]}
imageKey={images[visibleCount].objectKey || ""}
uuid={images[visibleCount].uuid || ""}
pageNumber={visibleCount + 1}
priority
/>
@ -478,19 +487,27 @@ const Chapter = () => {
/* MODE 2 --- STANDARD SINGLE-PAGE MODE */
/* ------------------------------------------------------------------ */
<>
<div className="relative mx-auto mb-8 overflow-hidden rounded-lg border border-border bg-muted">
<ChapterImage
imageKey={images[currentPage - 1]}
pageNumber={currentPage}
priority
/>
<div className="relative mx-auto mb-8 overflow-hidden rounded-lg border border-border bg-muted min-h-[300px]">
{images[currentPage - 1] ? (
<ChapterImage
imageKey={images[currentPage - 1].objectKey || ""}
uuid={images[currentPage - 1].uuid || ""}
pageNumber={currentPage}
priority
/>
) : (
<div className="flex h-64 items-center justify-center">
<p className="text-muted-foreground">Page not found</p>
</div>
)}
</div>
{/* Preload next image */}
{currentPage < images.length && (
<div style={{ display: "none" }} aria-hidden="true">
<ChapterImage
imageKey={images[currentPage]}
imageKey={images[currentPage].objectKey || ""}
uuid={images[currentPage].uuid || ""}
pageNumber={currentPage + 1}
priority
/>

View File

@ -59,7 +59,7 @@ const Manga = () => {
const { data: mangaData, queryKey, isLoading } = useGetManga(mangaId);
const { data: progressData } = useGetProgress(mangaId, {
query: { retry: false },
query: { retry: false, enabled: isAuthenticated },
});
const { mutate, isPending: fetchPending } =