Compare commits

..

No commits in common. "1cdfc905e4748e8dd5d3f851278f3e7a1addcbf1" and "5f33b87eceeed55f6727983a6b48103d780f499f" have entirely different histories.

6 changed files with 103 additions and 407 deletions

View File

@ -4,6 +4,10 @@
* OpenAPI definition * OpenAPI definition
* OpenAPI spec version: v0 * OpenAPI spec version: v0
*/ */
export interface UpdateMangaDataCommand {
mangaId?: number;
}
export interface DefaultResponseDTOVoid { export interface DefaultResponseDTOVoid {
timestamp?: string; timestamp?: string;
data?: unknown; data?: unknown;
@ -99,9 +103,9 @@ export interface PageMangaListDTO {
export interface PageableObject { export interface PageableObject {
offset?: number; offset?: number;
paged?: boolean;
pageNumber?: number; pageNumber?: number;
pageSize?: number; pageSize?: number;
paged?: boolean;
sort?: SortObject; sort?: SortObject;
unpaged?: boolean; unpaged?: boolean;
} }
@ -148,8 +152,6 @@ export interface MangaDTO {
score: number; score: number;
providers: MangaProviderDTO[]; providers: MangaProviderDTO[];
chapterCount: number; chapterCount: number;
favorite: boolean;
following: boolean;
} }
export interface MangaProviderDTO { export interface MangaProviderDTO {
@ -171,8 +173,6 @@ export interface MangaChapterImagesDTO {
id: number; id: number;
/** @minLength 1 */ /** @minLength 1 */
mangaTitle: string; mangaTitle: string;
previousChapterId?: number;
nextChapterId?: number;
chapterImageKeys: string[]; chapterImageKeys: string[];
} }

View File

@ -0,0 +1,86 @@
/**
* Generated by orval v7.15.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import {
useMutation
} from '@tanstack/react-query';
import type {
MutationFunction,
QueryClient,
UseMutationOptions,
UseMutationResult
} from '@tanstack/react-query';
import type {
UpdateMangaDataCommand
} from '../api.schemas';
import { customInstance } from '../../api';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
export const sendRecord = (
updateMangaDataCommand: UpdateMangaDataCommand,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<string>(
{url: `/records`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: updateMangaDataCommand, signal
},
options);
}
export const getSendRecordMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof sendRecord>>, TError,{data: UpdateMangaDataCommand}, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof sendRecord>>, TError,{data: UpdateMangaDataCommand}, TContext> => {
const mutationKey = ['sendRecord'];
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 sendRecord>>, {data: UpdateMangaDataCommand}> = (props) => {
const {data} = props ?? {};
return sendRecord(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type SendRecordMutationResult = NonNullable<Awaited<ReturnType<typeof sendRecord>>>
export type SendRecordMutationBody = UpdateMangaDataCommand
export type SendRecordMutationError = unknown
export const useSendRecord = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof sendRecord>>, TError,{data: UpdateMangaDataCommand}, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof sendRecord>>,
TError,
{data: UpdateMangaDataCommand},
TContext
> => {
const mutationOptions = getSendRecordMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}

View File

@ -1,154 +0,0 @@
/**
* Generated by orval v7.15.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
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];
/**
* Queue the retrieval of the manga lists from the content providers
* @summary Queue update manga list
*/
export const updateMangaList = (
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/management/update-manga-list`, method: 'POST', signal
},
options);
}
export const getUpdateMangaListMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMangaList>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof updateMangaList>>, TError,void, TContext> => {
const mutationKey = ['updateMangaList'];
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 updateMangaList>>, void> = () => {
return updateMangaList(requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type UpdateMangaListMutationResult = NonNullable<Awaited<ReturnType<typeof updateMangaList>>>
export type UpdateMangaListMutationError = unknown
/**
* @summary Queue update manga list
*/
export const useUpdateMangaList = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMangaList>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof updateMangaList>>,
TError,
void,
TContext
> => {
const mutationOptions = getUpdateMangaListMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* Triggers the cleanup of untracked S3 images
* @summary Cleanup unused S3 images
*/
export const imageCleanup = (
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/management/image-cleanup`, method: 'POST', signal
},
options);
}
export const getImageCleanupMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof imageCleanup>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof imageCleanup>>, TError,void, TContext> => {
const mutationKey = ['imageCleanup'];
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 imageCleanup>>, void> = () => {
return imageCleanup(requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type ImageCleanupMutationResult = NonNullable<Awaited<ReturnType<typeof imageCleanup>>>
export type ImageCleanupMutationError = unknown
/**
* @summary Cleanup unused S3 images
*/
export const useImageCleanup = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof imageCleanup>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof imageCleanup>>,
TError,
void,
TContext
> => {
const mutationOptions = getImageCleanupMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}

View File

@ -102,132 +102,6 @@ export const useFetchMangaChapters = <TError = unknown,
return useMutation(mutationOptions, queryClient); return useMutation(mutationOptions, queryClient);
} }
/** /**
* Unfollow the manga specified by its ID.
* @summary Unfollow the manga specified by its ID
*/
export const unfollowManga = (
mangaId: number,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/mangas/${encodeURIComponent(String(mangaId))}/unfollowManga`, method: 'POST', signal
},
options);
}
export const getUnfollowMangaMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof unfollowManga>>, TError,{mangaId: number}, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof unfollowManga>>, TError,{mangaId: number}, TContext> => {
const mutationKey = ['unfollowManga'];
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 unfollowManga>>, {mangaId: number}> = (props) => {
const {mangaId} = props ?? {};
return unfollowManga(mangaId,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type UnfollowMangaMutationResult = NonNullable<Awaited<ReturnType<typeof unfollowManga>>>
export type UnfollowMangaMutationError = unknown
/**
* @summary Unfollow the manga specified by its ID
*/
export const useUnfollowManga = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof unfollowManga>>, TError,{mangaId: number}, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof unfollowManga>>,
TError,
{mangaId: number},
TContext
> => {
const mutationOptions = getUnfollowMangaMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* Follow the manga specified by its ID.
* @summary Follow the manga specified by its ID
*/
export const followManga = (
mangaId: number,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/mangas/${encodeURIComponent(String(mangaId))}/followManga`, method: 'POST', signal
},
options);
}
export const getFollowMangaMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof followManga>>, TError,{mangaId: number}, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof followManga>>, TError,{mangaId: number}, TContext> => {
const mutationKey = ['followManga'];
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 followManga>>, {mangaId: number}> = (props) => {
const {mangaId} = props ?? {};
return followManga(mangaId,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type FollowMangaMutationResult = NonNullable<Awaited<ReturnType<typeof followManga>>>
export type FollowMangaMutationError = unknown
/**
* @summary Follow the manga specified by its ID
*/
export const useFollowManga = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof followManga>>, TError,{mangaId: number}, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof followManga>>,
TError,
{mangaId: number},
TContext
> => {
const mutationOptions = getFollowMangaMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* Retrieve a list of mangas with their details. * Retrieve a list of mangas with their details.
* @summary Get a list of mangas * @summary Get a list of mangas
*/ */

View File

@ -32,9 +32,10 @@ export const MangaDexImportDialog = ({
mangaDexDialogOpen, mangaDexDialogOpen,
onMangaDexDialogOpenChange, onMangaDexDialogOpenChange,
}: MangaDexImportDialogProps) => { }: MangaDexImportDialogProps) => {
const formSchema = z.object({ const formSchema = z
value: z.string().min(1, "Please enter a MangaDex ID or URL."), .object({
}); value: z.string().min(1, "Please enter a MangaDex ID or URL."),
});
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),

View File

@ -1,27 +1,18 @@
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { import {
ArrowLeft, ArrowLeft,
Bell,
BellOff,
BookOpen, BookOpen,
Calendar, Calendar,
ChevronDown, ChevronDown,
Database, Database,
Heart,
Star, Star,
} from "lucide-react"; } from "lucide-react";
import { useCallback, useState } from "react"; import { useState } from "react";
import { useNavigate, useParams } from "react-router"; import { useNavigate, useParams } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import {
useSetFavorite,
useSetUnfavorite,
} from "@/api/generated/favorite-mangas/favorite-mangas.ts";
import { import {
useFetchMangaChapters, useFetchMangaChapters,
useFollowManga,
useGetManga, useGetManga,
useUnfollowManga,
} from "@/api/generated/manga/manga.ts"; } from "@/api/generated/manga/manga.ts";
import { useFetchAllChapters } from "@/api/generated/manga-chapter/manga-chapter.ts"; import { useFetchAllChapters } from "@/api/generated/manga-chapter/manga-chapter.ts";
import { ThemeToggle } from "@/components/ThemeToggle.tsx"; import { ThemeToggle } from "@/components/ThemeToggle.tsx";
@ -33,12 +24,10 @@ import {
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible.tsx"; } from "@/components/ui/collapsible.tsx";
import { useAuth } from "@/contexts/AuthContext.tsx";
import { MangaChapter } from "@/features/manga/MangaChapter.tsx"; import { MangaChapter } from "@/features/manga/MangaChapter.tsx";
import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts"; import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts";
const Manga = () => { const Manga = () => {
const { isAuthenticated } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const params = useParams(); const params = useParams();
const mangaId = Number(params.mangaId); const mangaId = Number(params.mangaId);
@ -64,65 +53,6 @@ const Manga = () => {
const [openProviders, setOpenProviders] = useState<Set<number>>(new Set()); const [openProviders, setOpenProviders] = useState<Set<number>>(new Set());
const { mutate: mutateFavorite, isPending: isPendingFavorite } =
useSetFavorite({
mutation: {
onSuccess: () => queryClient.invalidateQueries({ queryKey }),
},
});
const { mutate: mutateUnfavorite, isPending: isPendingUnfavorite } =
useSetUnfavorite({
mutation: {
onSuccess: () => queryClient.invalidateQueries({ queryKey }),
},
});
const isPendingFavoriteChange = isPendingFavorite || isPendingUnfavorite;
const handleFavoriteClick = useCallback(
(isFavorite: boolean) => {
isFavorite
? mutateUnfavorite({ id: mangaData?.data?.id ?? -1 })
: mutateFavorite({ id: mangaData?.data?.id ?? -1 });
},
[mutateUnfavorite, mutateFavorite, mangaData?.data?.id],
);
const { mutate: mutateFollow, isPending: isPendingFollow } = useFollowManga({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey });
toast.success(
"We will notify you when new content if available for this manga.",
);
},
},
});
const { mutate: mutateUnfollow, isPending: isPendingUnfollow } =
useUnfollowManga({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey });
toast.success(
"You will no longer received notifications for this manga.",
);
},
},
});
const isPendingFollowChange = isPendingFollow || isPendingUnfollow;
const handleFollowClick = useCallback(
(isFollowing: boolean) => {
isFollowing
? mutateUnfollow({ mangaId: mangaData?.data?.id ?? -1 })
: mutateFollow({ mangaId: mangaData?.data?.id ?? -1 });
},
[mangaData?.data?.id, mutateUnfollow, mutateFollow],
);
if (!mangaData) { if (!mangaData) {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-background"> <div className="flex min-h-screen items-center justify-center bg-background">
@ -195,57 +125,16 @@ const Manga = () => {
{/* Details */} {/* Details */}
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<div className="mb-3 flex items-center justify-between gap-4"> <div className="mb-3 flex items-start justify-between gap-4">
<h1 className="text-balance text-4xl font-bold tracking-tight text-foreground"> <h1 className="text-balance text-4xl font-bold tracking-tight text-foreground">
{mangaData.data?.title} {mangaData.data?.title}
</h1> </h1>
<div className="flex gap-4 items-center"> <Badge
<Badge variant="secondary"
variant="secondary" className="border border-border bg-card text-foreground"
className="border border-border bg-card text-foreground max-h-6" >
> {mangaData.data?.status}
{mangaData.data?.status} </Badge>
</Badge>
{isAuthenticated && (
<>
<Button
size="icon"
variant="outline"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleFollowClick(
mangaData?.data?.following || false,
);
}}
disabled={isPendingFollowChange}
>
{mangaData?.data?.following ? <BellOff /> : <Bell />}
</Button>
<Button
size="icon"
variant="outline"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleFavoriteClick(
mangaData?.data?.favorite || false,
);
}}
disabled={isPendingFavoriteChange}
>
<Heart
className={`h-4 w-4 transition-colors ${
mangaData?.data?.favorite
? "fill-red-500 text-red-500"
: "text-muted-foreground hover:text-red-500"
}`}
/>
</Button>
</>
)}
</div>
</div> </div>
<p className="text-lg text-muted-foreground"> <p className="text-lg text-muted-foreground">
{mangaData.data?.authors.join(", ")} {mangaData.data?.authors.join(", ")}