feat/improvements #37
@ -26,20 +26,25 @@ export const MangaChapter = ({
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: mutateDownloadChapterArchive } = useDownloadContentArchive({
|
||||
mutation: {
|
||||
onSuccess: (data, { mangaContentId }) => {
|
||||
const url = window.URL.createObjectURL(data);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", `${mangaContentId}.cbz`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
const { mutate: mutateDownloadChapterArchive, isPending: isDownloading } =
|
||||
useDownloadContentArchive({
|
||||
mutation: {
|
||||
onMutate: ({ mangaContentId }) => setDownloadingId(mangaContentId),
|
||||
onSuccess: (data, { mangaContentId }) => {
|
||||
const url = window.URL.createObjectURL(data);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", `${mangaContentId}.cbz`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
onSettled: () => setDownloadingId(null),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const [downloadingId, setDownloadingId] = useState<number | null>(null);
|
||||
|
||||
const { mutate, isPending: isPendingFetchChapter } =
|
||||
useFetchContentProviderContent({
|
||||
@ -78,13 +83,12 @@ export const MangaChapter = ({
|
||||
return (
|
||||
<div
|
||||
key={chapter.id}
|
||||
className="flex items-center justify-between rounded-lg border border-border bg-background p-3"
|
||||
className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between rounded-lg border border-border bg-background p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full ${
|
||||
chapter.isRead ? "bg-primary/20" : "bg-muted"
|
||||
}`}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full ${chapter.isRead ? "bg-primary/20" : "bg-muted"
|
||||
}`}
|
||||
>
|
||||
{chapter.isRead ? (
|
||||
<Check className="h-4 w-4 text-primary" />
|
||||
@ -94,35 +98,57 @@ export const MangaChapter = ({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground flex items-center gap-2">
|
||||
{chapter.language?.code && (
|
||||
<ReactCountryFlag
|
||||
countryCode={chapter.language.code.split("-")[1]}
|
||||
svg
|
||||
style={{
|
||||
width: "1.2em",
|
||||
height: "1.2em",
|
||||
}}
|
||||
title={chapter.language.name}
|
||||
/>
|
||||
)}
|
||||
{chapter.title}
|
||||
</p>
|
||||
{chapter.downloaded && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
In database
|
||||
<div className="min-w-0 flex-1 flex items-center justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-foreground flex items-center gap-2 flex-wrap min-w-0">
|
||||
{chapter.language?.code && (
|
||||
<ReactCountryFlag
|
||||
countryCode={chapter.language.code.split("-")[1]}
|
||||
svg
|
||||
style={{
|
||||
width: "1.2em",
|
||||
height: "1.2em",
|
||||
}}
|
||||
title={chapter.language.name}
|
||||
/>
|
||||
)}
|
||||
<span className="truncate flex-1">{chapter.title}</span>
|
||||
</p>
|
||||
{chapter.downloaded && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
In database
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{chapter.downloaded && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
mutateDownloadChapterArchive({
|
||||
mangaContentId: chapter.id,
|
||||
params: { contentArchiveFileType: "CBZ" },
|
||||
})
|
||||
}
|
||||
className="sm:hidden h-8 w-8 p-0"
|
||||
disabled={isDownloading && downloadingId === chapter.id}
|
||||
>
|
||||
{isDownloading && downloadingId === chapter.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{chapter.downloaded ? (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:gap-2 w-full sm:w-auto">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleReadChapter(chapter.id)}
|
||||
className="gap-2"
|
||||
className="gap-2 w-full sm:w-auto"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
Read
|
||||
@ -136,9 +162,14 @@ export const MangaChapter = ({
|
||||
params: { contentArchiveFileType: "CBZ" },
|
||||
})
|
||||
}
|
||||
className="gap-2"
|
||||
className="hidden sm:flex gap-2 w-full sm:w-auto"
|
||||
disabled={isDownloading && downloadingId === chapter.id}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
{isDownloading && downloadingId === chapter.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4" />
|
||||
)}
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
@ -148,7 +179,7 @@ export const MangaChapter = ({
|
||||
variant="outline"
|
||||
onClick={() => fetchChapter(chapter.id)}
|
||||
disabled={isPendingFetchChapter}
|
||||
className="gap-2 cursor-pointer"
|
||||
className="gap-2 cursor-pointer w-full sm:w-auto"
|
||||
>
|
||||
<Database className="h-4 w-4" />
|
||||
{isPendingFetchChapter && fetchingId === chapter.id
|
||||
|
||||
@ -3,7 +3,6 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useGetMangaContentImages } from "@/api/generated/content/content.ts";
|
||||
import { useMarkContentAsRead } from "@/api/generated/user-interaction/user-interaction.ts";
|
||||
import { ThemeToggle } from "@/components/ThemeToggle.tsx";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@ -314,76 +313,85 @@ const Chapter = () => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* HEADER */}
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="px-4 py-4 sm:px-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/manga/${mangaId}`)}
|
||||
className="gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Back</span>
|
||||
</Button>
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 text-xs sm:text-sm">
|
||||
<div className="px-2 py-2 sm:px-4 sm:py-4">
|
||||
<div className="flex items-center justify-between gap-2 sm:gap-4">
|
||||
<div className="flex items-center gap-1 sm:gap-4 flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/manga/${mangaId}`)}
|
||||
className="gap-2 px-2 sm:px-3"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="hidden md:inline">Back</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate("/")}
|
||||
className="gap-2"
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Home</span>
|
||||
</Button>
|
||||
|
||||
<div className="hidden sm:block">
|
||||
<h1 className="text-sm font-semibold text-foreground">
|
||||
{data.data.mangaTitle}
|
||||
</h1>
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
{chapterNumber}
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate("/")}
|
||||
className="gap-2 px-1.5 sm:px-3"
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
<span className="hidden md:inline">Home</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{previousContentId && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={goToPreviousChapter}
|
||||
className="gap-1"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Prev</span>
|
||||
</Button>
|
||||
)}
|
||||
{nextContentId && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={goToNextChapter}
|
||||
className="gap-1"
|
||||
>
|
||||
<span className="hidden sm:inline">Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{previousContentId && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={goToPreviousChapter}
|
||||
className="gap-1 px-1.5"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Prev</span>
|
||||
</Button>
|
||||
)}
|
||||
{nextContentId && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={goToNextChapter}
|
||||
className="gap-1 px-1.5"
|
||||
>
|
||||
<span className="hidden sm:inline">Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title Info - now visible on all screens in the middle */}
|
||||
<div className="flex-1 min-w-0 px-1 text-center">
|
||||
<h1 className="text-[10px] sm:text-xs font-semibold text-foreground truncate">
|
||||
{data.data.mangaTitle}
|
||||
</h1>
|
||||
<p className="text-[8px] sm:text-[10px] text-muted-foreground truncate">
|
||||
Chapter {chapterNumber}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Page {currentPage} / {images.length}
|
||||
<div className="flex items-center gap-2 sm:gap-4 shrink-0">
|
||||
<span className="text-[10px] sm:text-sm text-muted-foreground whitespace-nowrap">
|
||||
{currentPage} / {images.length}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setInfiniteScroll((v) => !v)}
|
||||
className="text-xs"
|
||||
className="h-8 px-2 sm:px-3 text-[10px] sm:text-xs"
|
||||
>
|
||||
{infiniteScroll ? "Single Page Mode" : "Scroll Mode"}
|
||||
{infiniteScroll ? (
|
||||
<span className="">Single</span>
|
||||
) : (
|
||||
<span className="">Scroll</span>
|
||||
)}
|
||||
<span className="hidden sm:inline"> Mode</span>
|
||||
</Button>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -391,18 +399,10 @@ const Chapter = () => {
|
||||
|
||||
{/* MAIN */}
|
||||
<main
|
||||
className={`mx-auto max-w-4xl ${
|
||||
infiniteScroll ? "px-0 py-0" : "px-4 py-8"
|
||||
}`}
|
||||
className={`mx-auto max-w-4xl ${infiniteScroll ? "px-0 py-0" : "px-4 py-8"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-4 sm:hidden">
|
||||
<h1 className="text-lg font-semibold text-foreground">
|
||||
{data.data.mangaTitle}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{chapterNumber}
|
||||
</p>
|
||||
</div>
|
||||
{/* Removed mobile header from main as it's now in the sticky header */}
|
||||
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
{/* MODE 1 --- INFINITE SCROLL MODE */}
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
ChevronDown,
|
||||
Database,
|
||||
Heart,
|
||||
MoreVertical,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import { useCallback } from "react";
|
||||
@ -30,6 +31,12 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible.tsx";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu.tsx";
|
||||
import { useAuth } from "@/contexts/AuthContext.tsx";
|
||||
import { useUIState } from "@/contexts/UIStateContext.tsx";
|
||||
import { MangaChapter } from "@/features/manga/MangaChapter.tsx";
|
||||
@ -155,9 +162,9 @@ const Manga = () => {
|
||||
{/* Content Shell */}
|
||||
<main className="px-8 py-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 lg:grid-cols-[300px_1fr]">
|
||||
<div className="grid gap-8 md:grid-cols-[300px_1fr]">
|
||||
{/* Cover Skeleton */}
|
||||
<Skeleton className="aspect-2/3 w-full rounded-lg lg:sticky lg:top-8" />
|
||||
<Skeleton className="aspect-2/3 w-full rounded-lg md:sticky md:top-8" />
|
||||
|
||||
{/* Details Skeleton */}
|
||||
<div className="space-y-6">
|
||||
@ -252,10 +259,10 @@ const Manga = () => {
|
||||
<main className="px-8 py-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
{/* Manga Info Section */}
|
||||
<div className="grid gap-8 lg:grid-cols-[300px_1fr]">
|
||||
<div className="grid gap-8 md:grid-cols-[300px_1fr]">
|
||||
{/* Cover */}
|
||||
<div className="lg:sticky lg:top-24">
|
||||
<div className="relative aspect-2/3 overflow-hidden rounded-lg border border-border bg-muted">
|
||||
<div className="md:sticky md:top-24">
|
||||
<div className="relative aspect-2/3 overflow-hidden rounded-lg border border-border bg-muted w-full">
|
||||
<img
|
||||
src={
|
||||
(mangaData.data?.coverImageKey &&
|
||||
@ -273,14 +280,14 @@ const Manga = () => {
|
||||
{/* Details */}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="mb-3 flex items-center justify-between gap-4">
|
||||
<h1 className="text-balance text-4xl font-bold tracking-tight text-foreground">
|
||||
<div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h1 className="text-balance text-3xl sm:text-4xl font-bold tracking-tight text-foreground">
|
||||
{mangaData.data?.title}
|
||||
</h1>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="border border-border bg-card text-foreground max-h-6"
|
||||
className="border border-border bg-card text-foreground whitespace-nowrap"
|
||||
>
|
||||
{mangaData.data?.status}
|
||||
</Badge>
|
||||
@ -294,7 +301,7 @@ const Manga = () => {
|
||||
)}
|
||||
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<div className="flex gap-2 ml-auto sm:ml-0">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
@ -338,7 +345,7 @@ const Manga = () => {
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -395,6 +402,18 @@ const Manga = () => {
|
||||
dangerouslySetInnerHTML={{ __html: mangaData.data?.synopsis ?? "" }}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mangaData.data?.genres.map((genre) => (
|
||||
<Badge
|
||||
key={genre}
|
||||
variant="outline"
|
||||
className="border-border text-foreground"
|
||||
>
|
||||
{genre}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="flex items-center gap-3 rounded-lg border border-border bg-card p-4">
|
||||
<Star className="h-5 w-5 fill-primary text-primary" />
|
||||
@ -446,18 +465,6 @@ const Manga = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mangaData.data?.genres.map((genre) => (
|
||||
<Badge
|
||||
key={genre}
|
||||
variant="outline"
|
||||
className="border-border text-foreground"
|
||||
>
|
||||
{genre}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -489,83 +496,85 @@ const Manga = () => {
|
||||
asChild
|
||||
className={`flex-1 ${provider.chaptersAvailable > 0 ? "cursor-pointer" : "cursor-default"}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Database className="h-5 w-5 text-primary" />
|
||||
<div className="text-left">
|
||||
<p className="font-semibold text-foreground">
|
||||
{provider.providerName}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{provider.chaptersDownloaded} downloaded •{" "}
|
||||
{provider.chaptersAvailable} available
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Database className="h-5 w-5 text-primary" />
|
||||
<div className="text-left">
|
||||
<p className="font-semibold text-foreground">
|
||||
{provider.providerName}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{provider.chaptersDownloaded} downloaded •{" "}
|
||||
{provider.chaptersAvailable} available
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
{provider.supportsChapterFetch && provider.active && (
|
||||
<div className="flex gap-4 px-4">
|
||||
{provider.chaptersAvailable > 0 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={isPending || fetchAllPending}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
fetchAllMutate({
|
||||
mangaContentProviderId: provider.id ?? -1,
|
||||
});
|
||||
}}
|
||||
className="gap-2"
|
||||
>
|
||||
{fetchAllPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Database className="h-4 w-4" />
|
||||
)}
|
||||
Fetch all from Provider
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={isPending || fetchAllPending}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
mutate({
|
||||
mangaContentProviderId: provider.id ?? -1,
|
||||
});
|
||||
}}
|
||||
className="gap-2"
|
||||
<div className="flex items-center gap-2">
|
||||
<CollapsibleTrigger asChild>
|
||||
<div
|
||||
className={
|
||||
provider.chaptersAvailable > 0
|
||||
? "cursor-pointer"
|
||||
: "invisible"
|
||||
}
|
||||
>
|
||||
{isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Database className="h-4 w-4" />
|
||||
)}
|
||||
Fetch list from Provider
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<ChevronDown
|
||||
className={`h-5 w-5 text-muted-foreground transition-transform ${expandedProviderIds.includes(provider.id ?? -1)
|
||||
? "rotate-180"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleTrigger asChild>
|
||||
<div
|
||||
className={
|
||||
provider.chaptersAvailable > 0
|
||||
? "cursor-pointer"
|
||||
: "invisible"
|
||||
}
|
||||
>
|
||||
<ChevronDown
|
||||
className={`h-5 w-5 text-muted-foreground transition-transform ${expandedProviderIds.includes(provider.id ?? -1)
|
||||
? "rotate-180"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
{provider.supportsChapterFetch && provider.active && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={isPending || fetchAllPending}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{isPending || fetchAllPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{provider.chaptersAvailable > 0 && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
fetchAllMutate({
|
||||
mangaContentProviderId: provider.id ?? -1,
|
||||
});
|
||||
}}
|
||||
className="gap-2"
|
||||
>
|
||||
<Database className="h-4 w-4" />
|
||||
Fetch all from Provider
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
mutate({
|
||||
mangaContentProviderId: provider.id ?? -1,
|
||||
});
|
||||
}}
|
||||
className="gap-2"
|
||||
>
|
||||
<Database className="h-4 w-4" />
|
||||
Fetch list from Provider
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CollapsibleContent>
|
||||
<MangaChapter
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user