Compare commits
No commits in common. "5a06d2d73873b7e4ce6f0596eeb35ba91d7f446d" and "1cdfc905e4748e8dd5d3f851278f3e7a1addcbf1" have entirely different histories.
5a06d2d738
...
1cdfc905e4
@ -10,17 +10,17 @@ export interface DefaultResponseDTOVoid {
|
|||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportRequestDTO {
|
export interface ImportMangaDexRequestDTO {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DefaultResponseDTOImportMangaResponseDTO {
|
export interface DefaultResponseDTOImportMangaDexResponseDTO {
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
data?: ImportMangaResponseDTO;
|
data?: ImportMangaDexResponseDTO;
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportMangaResponseDTO {
|
export interface ImportMangaDexResponseDTO {
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,9 +99,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,69 +26,6 @@ type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger user follow update
|
|
||||||
* @summary Trigger user follow update
|
|
||||||
*/
|
|
||||||
export const userFollowUpdate = (
|
|
||||||
|
|
||||||
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
|
|
||||||
) => {
|
|
||||||
|
|
||||||
|
|
||||||
return customInstance<DefaultResponseDTOVoid>(
|
|
||||||
{url: `/management/user-follow`, method: 'POST', signal
|
|
||||||
},
|
|
||||||
options);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getUserFollowUpdateMutationOptions = <TError = unknown,
|
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof userFollowUpdate>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
|
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof userFollowUpdate>>, TError,void, TContext> => {
|
|
||||||
|
|
||||||
const mutationKey = ['userFollowUpdate'];
|
|
||||||
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 userFollowUpdate>>, void> = () => {
|
|
||||||
|
|
||||||
|
|
||||||
return userFollowUpdate(requestOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return { mutationFn, ...mutationOptions }}
|
|
||||||
|
|
||||||
export type UserFollowUpdateMutationResult = NonNullable<Awaited<ReturnType<typeof userFollowUpdate>>>
|
|
||||||
|
|
||||||
export type UserFollowUpdateMutationError = unknown
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Trigger user follow update
|
|
||||||
*/
|
|
||||||
export const useUserFollowUpdate = <TError = unknown,
|
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof userFollowUpdate>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
|
|
||||||
, queryClient?: QueryClient): UseMutationResult<
|
|
||||||
Awaited<ReturnType<typeof userFollowUpdate>>,
|
|
||||||
TError,
|
|
||||||
void,
|
|
||||||
TContext
|
|
||||||
> => {
|
|
||||||
|
|
||||||
const mutationOptions = getUserFollowUpdateMutationOptions(options);
|
|
||||||
|
|
||||||
return useMutation(mutationOptions, queryClient);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Queue the retrieval of the manga lists from the content providers
|
* Queue the retrieval of the manga lists from the content providers
|
||||||
* @summary Queue update manga list
|
* @summary Queue update manga list
|
||||||
*/
|
*/
|
||||||
@ -152,69 +89,6 @@ export const useUpdateMangaList = <TError = unknown,
|
|||||||
return useMutation(mutationOptions, queryClient);
|
return useMutation(mutationOptions, queryClient);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Sends a test notification to all users
|
|
||||||
* @summary Test notification
|
|
||||||
*/
|
|
||||||
export const testNotification = (
|
|
||||||
|
|
||||||
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
|
|
||||||
) => {
|
|
||||||
|
|
||||||
|
|
||||||
return customInstance<DefaultResponseDTOVoid>(
|
|
||||||
{url: `/management/test-notification`, method: 'POST', signal
|
|
||||||
},
|
|
||||||
options);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getTestNotificationMutationOptions = <TError = unknown,
|
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof testNotification>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
|
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof testNotification>>, TError,void, TContext> => {
|
|
||||||
|
|
||||||
const mutationKey = ['testNotification'];
|
|
||||||
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 testNotification>>, void> = () => {
|
|
||||||
|
|
||||||
|
|
||||||
return testNotification(requestOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return { mutationFn, ...mutationOptions }}
|
|
||||||
|
|
||||||
export type TestNotificationMutationResult = NonNullable<Awaited<ReturnType<typeof testNotification>>>
|
|
||||||
|
|
||||||
export type TestNotificationMutationError = unknown
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Test notification
|
|
||||||
*/
|
|
||||||
export const useTestNotification = <TError = unknown,
|
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof testNotification>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
|
|
||||||
, queryClient?: QueryClient): UseMutationResult<
|
|
||||||
Awaited<ReturnType<typeof testNotification>>,
|
|
||||||
TError,
|
|
||||||
void,
|
|
||||||
TContext
|
|
||||||
> => {
|
|
||||||
|
|
||||||
const mutationOptions = getTestNotificationMutationOptions(options);
|
|
||||||
|
|
||||||
return useMutation(mutationOptions, queryClient);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Triggers the cleanup of untracked S3 images
|
* Triggers the cleanup of untracked S3 images
|
||||||
* @summary Cleanup unused S3 images
|
* @summary Cleanup unused S3 images
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -15,10 +15,10 @@ import type {
|
|||||||
} from '@tanstack/react-query';
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
DefaultResponseDTOImportMangaResponseDTO,
|
DefaultResponseDTOImportMangaDexResponseDTO,
|
||||||
DefaultResponseDTOVoid,
|
DefaultResponseDTOVoid,
|
||||||
ImportMultipleFilesBody,
|
ImportMangaDexRequestDTO,
|
||||||
ImportRequestDTO
|
ImportMultipleFilesBody
|
||||||
} from '../api.schemas';
|
} from '../api.schemas';
|
||||||
|
|
||||||
import { customInstance } from '../../api';
|
import { customInstance } from '../../api';
|
||||||
@ -101,15 +101,15 @@ export const useImportMultipleFiles = <TError = unknown,
|
|||||||
* @summary Import manga from MangaDex
|
* @summary Import manga from MangaDex
|
||||||
*/
|
*/
|
||||||
export const importFromMangaDex = (
|
export const importFromMangaDex = (
|
||||||
importRequestDTO: ImportRequestDTO,
|
importMangaDexRequestDTO: ImportMangaDexRequestDTO,
|
||||||
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
|
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
|
||||||
) => {
|
) => {
|
||||||
|
|
||||||
|
|
||||||
return customInstance<DefaultResponseDTOImportMangaResponseDTO>(
|
return customInstance<DefaultResponseDTOImportMangaDexResponseDTO>(
|
||||||
{url: `/manga/import/manga-dex`, method: 'POST',
|
{url: `/manga/import/manga-dex`, method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json', },
|
headers: {'Content-Type': 'application/json', },
|
||||||
data: importRequestDTO, signal
|
data: importMangaDexRequestDTO, signal
|
||||||
},
|
},
|
||||||
options);
|
options);
|
||||||
}
|
}
|
||||||
@ -117,8 +117,8 @@ export const importFromMangaDex = (
|
|||||||
|
|
||||||
|
|
||||||
export const getImportFromMangaDexMutationOptions = <TError = unknown,
|
export const getImportFromMangaDexMutationOptions = <TError = unknown,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importFromMangaDex>>, TError,{data: ImportRequestDTO}, TContext>, request?: SecondParameter<typeof customInstance>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importFromMangaDex>>, TError,{data: ImportMangaDexRequestDTO}, TContext>, request?: SecondParameter<typeof customInstance>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof importFromMangaDex>>, TError,{data: ImportRequestDTO}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof importFromMangaDex>>, TError,{data: ImportMangaDexRequestDTO}, TContext> => {
|
||||||
|
|
||||||
const mutationKey = ['importFromMangaDex'];
|
const mutationKey = ['importFromMangaDex'];
|
||||||
const {mutation: mutationOptions, request: requestOptions} = options ?
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
||||||
@ -130,7 +130,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const mutationFn: MutationFunction<Awaited<ReturnType<typeof importFromMangaDex>>, {data: ImportRequestDTO}> = (props) => {
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof importFromMangaDex>>, {data: ImportMangaDexRequestDTO}> = (props) => {
|
||||||
const {data} = props ?? {};
|
const {data} = props ?? {};
|
||||||
|
|
||||||
return importFromMangaDex(data,requestOptions)
|
return importFromMangaDex(data,requestOptions)
|
||||||
@ -142,18 +142,18 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
|
|||||||
return { mutationFn, ...mutationOptions }}
|
return { mutationFn, ...mutationOptions }}
|
||||||
|
|
||||||
export type ImportFromMangaDexMutationResult = NonNullable<Awaited<ReturnType<typeof importFromMangaDex>>>
|
export type ImportFromMangaDexMutationResult = NonNullable<Awaited<ReturnType<typeof importFromMangaDex>>>
|
||||||
export type ImportFromMangaDexMutationBody = ImportRequestDTO
|
export type ImportFromMangaDexMutationBody = ImportMangaDexRequestDTO
|
||||||
export type ImportFromMangaDexMutationError = unknown
|
export type ImportFromMangaDexMutationError = unknown
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Import manga from MangaDex
|
* @summary Import manga from MangaDex
|
||||||
*/
|
*/
|
||||||
export const useImportFromMangaDex = <TError = unknown,
|
export const useImportFromMangaDex = <TError = unknown,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importFromMangaDex>>, TError,{data: ImportRequestDTO}, TContext>, request?: SecondParameter<typeof customInstance>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importFromMangaDex>>, TError,{data: ImportMangaDexRequestDTO}, TContext>, request?: SecondParameter<typeof customInstance>}
|
||||||
, queryClient?: QueryClient): UseMutationResult<
|
, queryClient?: QueryClient): UseMutationResult<
|
||||||
Awaited<ReturnType<typeof importFromMangaDex>>,
|
Awaited<ReturnType<typeof importFromMangaDex>>,
|
||||||
TError,
|
TError,
|
||||||
{data: ImportRequestDTO},
|
{data: ImportMangaDexRequestDTO},
|
||||||
TContext
|
TContext
|
||||||
> => {
|
> => {
|
||||||
|
|
||||||
@ -161,69 +161,4 @@ export const useImportFromMangaDex = <TError = unknown,
|
|||||||
|
|
||||||
return useMutation(mutationOptions, queryClient);
|
return useMutation(mutationOptions, queryClient);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Imports manga data from Bato into the local database.
|
|
||||||
* @summary Import manga from Bato
|
|
||||||
*/
|
|
||||||
export const importFromBato = (
|
|
||||||
importRequestDTO: ImportRequestDTO,
|
|
||||||
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
|
|
||||||
) => {
|
|
||||||
|
|
||||||
|
|
||||||
return customInstance<DefaultResponseDTOImportMangaResponseDTO>(
|
|
||||||
{url: `/manga/import/bato`, method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json', },
|
|
||||||
data: importRequestDTO, signal
|
|
||||||
},
|
|
||||||
options);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getImportFromBatoMutationOptions = <TError = unknown,
|
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importFromBato>>, TError,{data: ImportRequestDTO}, TContext>, request?: SecondParameter<typeof customInstance>}
|
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof importFromBato>>, TError,{data: ImportRequestDTO}, TContext> => {
|
|
||||||
|
|
||||||
const mutationKey = ['importFromBato'];
|
|
||||||
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 importFromBato>>, {data: ImportRequestDTO}> = (props) => {
|
|
||||||
const {data} = props ?? {};
|
|
||||||
|
|
||||||
return importFromBato(data,requestOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return { mutationFn, ...mutationOptions }}
|
|
||||||
|
|
||||||
export type ImportFromBatoMutationResult = NonNullable<Awaited<ReturnType<typeof importFromBato>>>
|
|
||||||
export type ImportFromBatoMutationBody = ImportRequestDTO
|
|
||||||
export type ImportFromBatoMutationError = unknown
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Import manga from Bato
|
|
||||||
*/
|
|
||||||
export const useImportFromBato = <TError = unknown,
|
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importFromBato>>, TError,{data: ImportRequestDTO}, TContext>, request?: SecondParameter<typeof customInstance>}
|
|
||||||
, queryClient?: QueryClient): UseMutationResult<
|
|
||||||
Awaited<ReturnType<typeof importFromBato>>,
|
|
||||||
TError,
|
|
||||||
{data: ImportRequestDTO},
|
|
||||||
TContext
|
|
||||||
> => {
|
|
||||||
|
|
||||||
const mutationOptions = getImportFromBatoMutationOptions(options);
|
|
||||||
|
|
||||||
return useMutation(mutationOptions, queryClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { useImportFromBato } from "@/api/generated/manga-import/manga-import.ts";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog.tsx";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form.tsx";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
|
|
||||||
interface BatoImportDialogProps {
|
|
||||||
batoDialogOpen: boolean;
|
|
||||||
onBatoDialogOpenChange: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BatoImportDialog = ({
|
|
||||||
batoDialogOpen,
|
|
||||||
onBatoDialogOpenChange,
|
|
||||||
}: BatoImportDialogProps) => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
value: z.string().min(1, "Please enter a Bato URL."),
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: importBato, isPending: isPendingImportBato } =
|
|
||||||
useImportFromBato({
|
|
||||||
mutation: {
|
|
||||||
onSuccess: () => {
|
|
||||||
form.reset();
|
|
||||||
onBatoDialogOpenChange(false);
|
|
||||||
toast.success("Manga imported successfully!");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
|
||||||
(data: z.infer<typeof formSchema>) => {
|
|
||||||
const id = data.value;
|
|
||||||
|
|
||||||
importBato({ data: { id } });
|
|
||||||
},
|
|
||||||
[formSchema, importBato],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={batoDialogOpen} onOpenChange={onBatoDialogOpenChange}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Import from Bato</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Enter a Bato manga URL to import it to your library.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="importForm"
|
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="value"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Bato URL </FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="e.g., https://bato.two/title/..."
|
|
||||||
disabled={isPendingImportBato}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBatoDialogOpenChange(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isPendingImportBato}
|
|
||||||
form="importForm"
|
|
||||||
>
|
|
||||||
Import
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -10,7 +10,6 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useAuth } from "@/contexts/AuthContext.tsx";
|
import { useAuth } from "@/contexts/AuthContext.tsx";
|
||||||
import { BatoImportDialog } from "@/features/home/components/BatoImportDialog.tsx";
|
|
||||||
import { MangaDexImportDialog } from "@/features/home/components/MangaDexImportDialog.tsx";
|
import { MangaDexImportDialog } from "@/features/home/components/MangaDexImportDialog.tsx";
|
||||||
import { MangaManualImportDialog } from "@/features/home/components/MangaManualImportDialog.tsx";
|
import { MangaManualImportDialog } from "@/features/home/components/MangaManualImportDialog.tsx";
|
||||||
|
|
||||||
@ -18,7 +17,6 @@ export function ImportDropdown() {
|
|||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
const [mangaDexDialogOpen, setMangaDexDialogOpen] = useState(false);
|
const [mangaDexDialogOpen, setMangaDexDialogOpen] = useState(false);
|
||||||
const [batoDialogOpen, setBatoDialogOpen] = useState(false);
|
|
||||||
const [fileImportDialogOpen, setFileImportDialogOpen] = useState(false);
|
const [fileImportDialogOpen, setFileImportDialogOpen] = useState(false);
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
@ -39,10 +37,6 @@ export function ImportDropdown() {
|
|||||||
<Download className="mr-2 h-4 w-4" />
|
<Download className="mr-2 h-4 w-4" />
|
||||||
Import from MangaDex
|
Import from MangaDex
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setBatoDialogOpen(true)}>
|
|
||||||
<Download className="mr-2 h-4 w-4" />
|
|
||||||
Import from Bato
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setFileImportDialogOpen(true)}>
|
<DropdownMenuItem onClick={() => setFileImportDialogOpen(true)}>
|
||||||
<FileUp className="mr-2 h-4 w-4" />
|
<FileUp className="mr-2 h-4 w-4" />
|
||||||
Import from File
|
Import from File
|
||||||
@ -66,11 +60,6 @@ export function ImportDropdown() {
|
|||||||
onMangaDexDialogOpenChange={setMangaDexDialogOpen}
|
onMangaDexDialogOpenChange={setMangaDexDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BatoImportDialog
|
|
||||||
batoDialogOpen={batoDialogOpen}
|
|
||||||
onBatoDialogOpenChange={setBatoDialogOpen}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MangaManualImportDialog
|
<MangaManualImportDialog
|
||||||
fileImportDialogOpen={fileImportDialogOpen}
|
fileImportDialogOpen={fileImportDialogOpen}
|
||||||
onFileImportDialogOpenChange={setFileImportDialogOpen}
|
onFileImportDialogOpenChange={setFileImportDialogOpen}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ArrowLeft, ChevronLeft, ChevronRight, Home } from "lucide-react";
|
import { ArrowLeft, ChevronLeft, ChevronRight, Home } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import {
|
import {
|
||||||
useGetMangaChapterImages,
|
useGetMangaChapterImages,
|
||||||
@ -21,95 +21,34 @@ const Chapter = () => {
|
|||||||
getCurrentChapterPage(chapterNumber) ?? 1,
|
getCurrentChapterPage(chapterNumber) ?? 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [infiniteScroll, setInfiniteScroll] = useState(true);
|
|
||||||
|
|
||||||
const { data, isLoading } = useGetMangaChapterImages(chapterNumber);
|
const { data, isLoading } = useGetMangaChapterImages(chapterNumber);
|
||||||
|
|
||||||
const { mutate } = useMarkAsRead();
|
const { mutate } = useMarkAsRead();
|
||||||
|
|
||||||
// For infinite scroll mode
|
|
||||||
const [visibleCount, setVisibleCount] = useState(1);
|
|
||||||
const loadMoreRef = useRef(null);
|
|
||||||
|
|
||||||
/** Mark chapter as read when last page reached */
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data || isLoading) return;
|
if (!data || isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentPage === data.data?.chapterImageKeys.length) {
|
if (currentPage === data.data?.chapterImageKeys.length) {
|
||||||
mutate({ chapterId: chapterNumber });
|
mutate({ chapterId: chapterNumber });
|
||||||
}
|
}
|
||||||
}, [data, mutate, currentPage]);
|
}, [data, mutate, currentPage]);
|
||||||
|
|
||||||
/** Persist reading progress */
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentChapterPage(chapterNumber, currentPage);
|
setCurrentChapterPage(chapterNumber, currentPage);
|
||||||
}, [chapterNumber, currentPage]);
|
}, [chapterNumber, currentPage]);
|
||||||
|
|
||||||
/** Restore stored page */
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && data?.data) {
|
if (!isLoading && !data?.data) {
|
||||||
const stored = getCurrentChapterPage(chapterNumber);
|
return;
|
||||||
if (stored) {
|
|
||||||
setCurrentPage(stored);
|
|
||||||
setVisibleCount(stored); // for infinite scroll
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [isLoading, data?.data]);
|
|
||||||
|
|
||||||
/** Infinite scroll observer */
|
const storedChapterPage = getCurrentChapterPage(chapterNumber);
|
||||||
useEffect(() => {
|
if (storedChapterPage) {
|
||||||
if (!infiniteScroll) return;
|
setCurrentPage(storedChapterPage);
|
||||||
if (!loadMoreRef.current) return;
|
|
||||||
|
|
||||||
const obs = new IntersectionObserver((entries) => {
|
|
||||||
if (entries[0].isIntersecting) {
|
|
||||||
setVisibleCount((count) =>
|
|
||||||
Math.min(count + 2, data?.data?.chapterImageKeys.length ?? 0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
obs.observe(loadMoreRef.current);
|
|
||||||
return () => obs.disconnect();
|
|
||||||
}, [infiniteScroll, data?.data]);
|
|
||||||
|
|
||||||
/** Track which image is currently visible (for progress update) */
|
|
||||||
useEffect(() => {
|
|
||||||
if (!infiniteScroll) return;
|
|
||||||
|
|
||||||
const imgs = document.querySelectorAll("[data-page]");
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
const el = entry.target as HTMLElement; // <-- FIX
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
const pageNum = Number(el.dataset.page); // <-- SAFE
|
|
||||||
setCurrentPage(pageNum);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ threshold: 0.5 },
|
|
||||||
);
|
|
||||||
|
|
||||||
imgs.forEach((img) => observer.observe(img));
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, [infiniteScroll, visibleCount]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data?.data) return;
|
|
||||||
|
|
||||||
// When switching modes:
|
|
||||||
if (infiniteScroll) {
|
|
||||||
// Scroll mode → show saved progress
|
|
||||||
setVisibleCount(currentPage);
|
|
||||||
setTimeout(() => {
|
|
||||||
const el = document.querySelector(`[data-page="${currentPage}"]`);
|
|
||||||
el?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
||||||
}, 50);
|
|
||||||
} else {
|
|
||||||
// Single page mode → scroll to top
|
|
||||||
window.scrollTo({ top: 0 });
|
|
||||||
}
|
}
|
||||||
}, [infiniteScroll]);
|
}, [getCurrentChapterPage, isLoading, data?.data]);
|
||||||
|
|
||||||
if (!data?.data) {
|
if (!data?.data) {
|
||||||
return (
|
return (
|
||||||
@ -126,25 +65,27 @@ const Chapter = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const images = data.data.chapterImageKeys;
|
|
||||||
|
|
||||||
/** Standard navigation (non-infinite mode) */
|
|
||||||
const goToNextPage = () => {
|
const goToNextPage = () => {
|
||||||
if (currentPage < images.length) {
|
if (!data?.data) {
|
||||||
setCurrentPage((p) => p + 1);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < data.data.chapterImageKeys.length) {
|
||||||
|
setCurrentPage(currentPage + 1);
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToPreviousPage = () => {
|
const goToPreviousPage = () => {
|
||||||
if (currentPage > 1) {
|
if (currentPage > 1) {
|
||||||
setCurrentPage((p) => p - 1);
|
setCurrentPage(currentPage - 1);
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
{/* HEADER */}
|
{/* Header */}
|
||||||
<header className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<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="px-4 py-4 sm:px-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -158,7 +99,6 @@ const Chapter = () => {
|
|||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">Back</span>
|
<span className="hidden sm:inline">Back</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -168,7 +108,6 @@ const Chapter = () => {
|
|||||||
<Home className="h-4 w-4" />
|
<Home className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">Home</span>
|
<span className="hidden sm:inline">Home</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">
|
||||||
<h1 className="text-sm font-semibold text-foreground">
|
<h1 className="text-sm font-semibold text-foreground">
|
||||||
{data.data.mangaTitle}
|
{data.data.mangaTitle}
|
||||||
@ -178,27 +117,19 @@ const Chapter = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Page {currentPage} / {images.length}
|
Page {currentPage} / {data.data.chapterImageKeys.length}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setInfiniteScroll((v) => !v)}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{infiniteScroll ? "Single Page Mode" : "Scroll Mode"}
|
|
||||||
</Button>
|
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* MAIN */}
|
{/* Reader Content */}
|
||||||
<main className="mx-auto max-w-4xl px-4 py-8">
|
<main className="mx-auto max-w-4xl px-4 py-8">
|
||||||
|
{/* Mobile title */}
|
||||||
<div className="mb-4 sm:hidden">
|
<div className="mb-4 sm:hidden">
|
||||||
<h1 className="text-lg font-semibold text-foreground">
|
<h1 className="text-lg font-semibold text-foreground">
|
||||||
{data.data.mangaTitle}
|
{data.data.mangaTitle}
|
||||||
@ -208,74 +139,74 @@ const Chapter = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* Manga Page */}
|
||||||
{/* MODE 1 --- INFINITE SCROLL MODE */}
|
<div className="relative mx-auto mb-8 overflow-hidden rounded-lg border border-border bg-muted">
|
||||||
{/* ------------------------------------------------------------------ */}
|
<img
|
||||||
{infiniteScroll ? (
|
src={
|
||||||
<div className="flex flex-col space-y-0">
|
import.meta.env.VITE_OMV_BASE_URL +
|
||||||
{images.slice(0, visibleCount).map((key, idx) => (
|
"/" +
|
||||||
<img
|
data.data.chapterImageKeys[currentPage - 1] ||
|
||||||
key={idx}
|
"/placeholder.svg"
|
||||||
data-page={idx + 1}
|
}
|
||||||
src={`${import.meta.env.VITE_OMV_BASE_URL}/${key}`}
|
alt={`Page ${currentPage}`}
|
||||||
className="w-full h-auto block"
|
width={1000}
|
||||||
alt={`Page ${idx + 1}`}
|
height={1400}
|
||||||
loading="lazy"
|
className="h-auto w-full"
|
||||||
/>
|
// priority
|
||||||
))}
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* LOAD MORE SENTINEL */}
|
{/* Navigation Controls */}
|
||||||
<div ref={loadMoreRef} className="h-10" />
|
<div className="space-y-4">
|
||||||
|
{/* Page Navigation */}
|
||||||
|
<div className="flex items-center justify-center gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={goToPreviousPage}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2 bg-transparent"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
Previous Page
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-medium text-foreground">
|
||||||
|
{currentPage} / {data.data.chapterImageKeys.length}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
onClick={goToNextPage}
|
||||||
|
disabled={currentPage === data.data.chapterImageKeys.length}
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2 bg-transparent"
|
||||||
|
>
|
||||||
|
Next Page
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* MODE 2 --- STANDARD SINGLE-PAGE MODE */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
<>
|
|
||||||
<div className="relative mx-auto mb-8 overflow-hidden rounded-lg border border-border bg-muted">
|
|
||||||
<img
|
|
||||||
src={
|
|
||||||
import.meta.env.VITE_OMV_BASE_URL +
|
|
||||||
"/" +
|
|
||||||
images[currentPage - 1] || "/placeholder.svg"
|
|
||||||
}
|
|
||||||
alt={`Page ${currentPage}`}
|
|
||||||
width={1000}
|
|
||||||
height={1400}
|
|
||||||
className="h-auto w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* NAVIGATION BUTTONS */}
|
{/*/!* Chapter Navigation *!/*/}
|
||||||
<div className="space-y-4">
|
{/*{(currentPage === data.chapterImageKeys.length || currentPage === 1) && (*/}
|
||||||
<div className="flex items-center justify-center gap-4">
|
{/* <div className="flex items-center justify-center gap-4 border-t border-border pt-4">*/}
|
||||||
<Button
|
{/* <Button*/}
|
||||||
onClick={goToPreviousPage}
|
{/* onClick={goToPreviousChapter}*/}
|
||||||
disabled={currentPage === 1}
|
{/* disabled={chapterNumber === 1}*/}
|
||||||
variant="outline"
|
{/* variant="secondary"*/}
|
||||||
className="gap-2 bg-transparent"
|
{/* className="gap-2"*/}
|
||||||
>
|
{/* >*/}
|
||||||
<ChevronLeft className="h-4 w-4" />
|
{/* <ChevronLeft className="h-4 w-4" />*/}
|
||||||
Previous Page
|
{/* Previous Chapter*/}
|
||||||
</Button>
|
{/* </Button>*/}
|
||||||
|
{/* <Button*/}
|
||||||
<span className="text-sm font-medium text-foreground">
|
{/* onClick={goToNextChapter}*/}
|
||||||
{currentPage} / {images.length}
|
{/* disabled={chapterNumber === manga.chapters}*/}
|
||||||
</span>
|
{/* variant="secondary"*/}
|
||||||
|
{/* className="gap-2"*/}
|
||||||
<Button
|
{/* >*/}
|
||||||
onClick={goToNextPage}
|
{/* Next Chapter*/}
|
||||||
disabled={currentPage === images.length}
|
{/* <ChevronRight className="h-4 w-4" />*/}
|
||||||
variant="outline"
|
{/* </Button>*/}
|
||||||
className="gap-2 bg-transparent"
|
{/* </div>*/}
|
||||||
>
|
{/*)}*/}
|
||||||
Next Page
|
</div>
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user