fetchApi dont throw rejected promises

This commit is contained in:
glax 2025-05-18 19:49:43 +02:00
parent a0d15e08a7
commit f391ace9b2
22 changed files with 109 additions and 59 deletions

View File

@ -5,7 +5,7 @@ import {
CardContent, CardCover, CardContent, CardCover,
Link, Link,
} from "@mui/joy"; } from "@mui/joy";
import IManga, {DefaultManga} from "../api/types/IManga.ts"; import IManga from "../api/types/IManga.ts";
import {CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react"; import {CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
import {GetMangaById, GetMangaCoverImageUrl} from "../api/Manga.tsx"; import {GetMangaById, GetMangaCoverImageUrl} from "../api/Manga.tsx";
import {ApiUriContext, getData} from "../api/fetchApi.tsx"; import {ApiUriContext, getData} from "../api/fetchApi.tsx";
@ -16,7 +16,7 @@ import IMangaConnector from "../api/types/IMangaConnector.ts";
import {GetConnector} from "../api/MangaConnector.tsx"; import {GetConnector} from "../api/MangaConnector.tsx";
export function MangaFromId({mangaId, children} : { mangaId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){ export function MangaFromId({mangaId, children} : { mangaId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
const [manga, setManga] = useState(DefaultManga); const [manga, setManga] = useState<IManga>();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const apiUri = useContext(ApiUriContext); const apiUri = useContext(ApiUriContext);
@ -32,7 +32,7 @@ export function MangaFromId({mangaId, children} : { mangaId: string, children?:
return ( return (
<> <>
{loading ? <></> : <Manga manga={manga} children={children} /> } {loading || manga === undefined ? <></> : <Manga manga={manga} children={children} /> }
</> </>
); );
} }

View File

@ -35,21 +35,23 @@ export default function MangaList({connected, children}: {connected: boolean, ch
const timerRef = React.useRef<ReturnType<typeof setInterval>>(undefined); const timerRef = React.useRef<ReturnType<typeof setInterval>>(undefined);
const updateTimer = () => { const updateTimer = () => {
if(!connected){ if(!connected){
console.debug("Clear timer");
clearTimeout(timerRef.current); clearTimeout(timerRef.current);
return; return;
}else{ }else{
console.debug("Add timer"); if(timerRef.current === undefined) {
timerRef.current = setInterval(() => { console.log("Added timer!");
getJobList(); getJobList();
}, 2000); timerRef.current = setInterval(() => {
getJobList();
}, 2000);
}
} }
} }
return( return(
<Stack direction="row" spacing={1} flexWrap={"wrap"}> <Stack direction="row" spacing={1} flexWrap={"wrap"}>
{children} {children}
{jobList.map((job) => ( {jobList?.map((job) => (
<MangaFromId key={job.mangaId} mangaId={job.mangaId}> <MangaFromId key={job.mangaId} mangaId={job.mangaId}>
<Button color={"danger"} endDecorator={<Remove />} onClick={() => deleteJob(job.jobId)}>Delete</Button> <Button color={"danger"} endDecorator={<Remove />} onClick={() => deleteJob(job.jobId)}>Delete</Button>
</MangaFromId> </MangaFromId>

View File

@ -35,7 +35,7 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
const [step, setStep] = useState<number>(1); const [step, setStep] = useState<number>(1);
const apiUri = useContext(ApiUriContext); const apiUri = useContext(ApiUriContext);
const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>([]); const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>();
const [mangaConnectorsLoading, setMangaConnectorsLoading] = useState<boolean>(true); const [mangaConnectorsLoading, setMangaConnectorsLoading] = useState<boolean>(true);
const [selectedMangaConnector, setSelectedMangaConnector] = useState<IMangaConnector>(); const [selectedMangaConnector, setSelectedMangaConnector] = useState<IMangaConnector>();
@ -83,7 +83,7 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
return ( return (
<React.Fragment> <React.Fragment>
<ListItemDecorator> <ListItemDecorator>
<Avatar size="sm" src={mangaConnectors.find((o) => o.name === option.value)?.iconUrl} /> <Avatar size="sm" src={mangaConnectors?.find((o) => o.name === option.value)?.iconUrl} />
</ListItemDecorator> </ListItemDecorator>
{option.label} {option.label}
</React.Fragment> </React.Fragment>
@ -101,13 +101,13 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
<ModalClose /> <ModalClose />
<Stepper orientation={"vertical"} sx={{ height: '100%', width: "calc(100% - 80px)", margin:"40px"}}> <Stepper orientation={"vertical"} sx={{ height: '100%', width: "calc(100% - 80px)", margin:"40px"}}>
<Step indicator={ <Step indicator={
<StepIndicator variant={step==1?"solid":"outlined"} color={mangaConnectors.length < 1 ? "danger" : "primary"}> <StepIndicator variant={step==1?"solid":"outlined"} color={mangaConnectors?.length??0 < 1 ? "danger" : "primary"}>
1 1
</StepIndicator>}> </StepIndicator>}>
<Skeleton loading={mangaConnectorsLoading}> <Skeleton loading={mangaConnectorsLoading}>
<Select <Select
color={mangaConnectors.length < 1 ? "danger" : "neutral"} color={mangaConnectors?.length??0 < 1 ? "danger" : "neutral"}
disabled={mangaConnectorsLoading || resultsLoading || mangaConnectors.length < 1} disabled={mangaConnectorsLoading || resultsLoading || mangaConnectors?.length == null || mangaConnectors.length < 1}
placeholder={"Select Connector"} placeholder={"Select Connector"}
slotProps={{ slotProps={{
listbox: { listbox: {
@ -120,9 +120,9 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
renderValue={renderValue} renderValue={renderValue}
onChange={(_e, newValue) => { onChange={(_e, newValue) => {
setStep(2); setStep(2);
setSelectedMangaConnector(mangaConnectors.find((o) => o.name === newValue)); setSelectedMangaConnector(mangaConnectors?.find((o) => o.name === newValue));
}} }}
endDecorator={<Chip size={"sm"} color={mangaConnectors.length < 1 ? "danger" : "primary"}>{mangaConnectors.length}</Chip>}> endDecorator={<Chip size={"sm"} color={mangaConnectors?.length??0 < 1 ? "danger" : "primary"}>{mangaConnectors?.length}</Chip>}>
{mangaConnectors?.map((connector: IMangaConnector) => ConnectorOption(connector))} {mangaConnectors?.map((connector: IMangaConnector) => ConnectorOption(connector))}
</Select> </Select>
</Skeleton> </Skeleton>

View File

@ -0,0 +1,4 @@
import {createContext} from "react";
import IMangaConnector from "../types/IMangaConnector.ts";
export const MangaConnectorContext = createContext<IMangaConnector[]>([]);

View File

@ -88,10 +88,10 @@ export const CreateUpdateAllMetadataJob = async (apiUri: string) : Promise<strin
return await putData(`${apiUri}/v2/Job/UpdateAllMetadataJob`, {}) as Promise<string[]>; return await putData(`${apiUri}/v2/Job/UpdateAllMetadataJob`, {}) as Promise<string[]>;
} }
export const StartJob = async (apiUri: string, jobId: string) : Promise<object> => { export const StartJob = async (apiUri: string, jobId: string) : Promise<object | undefined> => {
return await postData(`${apiUri}/v2/Job/${jobId}/Start`, {}); return await postData(`${apiUri}/v2/Job/${jobId}/Start`, {});
} }
export const StopJob = async (apiUri: string, jobId: string) : Promise<object> => { export const StopJob = async (apiUri: string, jobId: string) : Promise<object | undefined> => {
return await postData(`${apiUri}/v2/Job/${jobId}/Stop`, {}); return await postData(`${apiUri}/v2/Job/${jobId}/Stop`, {});
} }

View File

@ -18,14 +18,14 @@ export const DeleteLibrary = async (apiUri: string, libraryId: string) : Promis
return await deleteData(`${apiUri}/v2/LocalLibraries/${libraryId}`); return await deleteData(`${apiUri}/v2/LocalLibraries/${libraryId}`);
} }
export const ChangeLibraryPath = async (apiUri: string, libraryId: string, newPath: string) : Promise<object> => { export const ChangeLibraryPath = async (apiUri: string, libraryId: string, newPath: string) : Promise<object | undefined> => {
return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}/ChangeBasePath`, newPath); return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}/ChangeBasePath`, newPath);
} }
export const ChangeLibraryName = async (apiUri: string, libraryId: string, newName: string) : Promise<object> => { export const ChangeLibraryName = async (apiUri: string, libraryId: string, newName: string) : Promise<object | undefined> => {
return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}/ChangeName`, newName); return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}/ChangeName`, newName);
} }
export const UpdateLibrary = async (apiUri: string, libraryId: string, record: INewLibraryRecord) : Promise<object> => { export const UpdateLibrary = async (apiUri: string, libraryId: string, record: INewLibraryRecord) : Promise<object | undefined> => {
return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}`, record); return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}`, record);
} }

View File

@ -1,5 +1,5 @@
import {deleteData, getData, patchData, postData} from './fetchApi.tsx'; import {deleteData, getData, patchData, postData} from './fetchApi.tsx';
import IManga from "./types/IManga.ts"; import IManga, {DefaultManga} from "./types/IManga.ts";
import IChapter from "./types/IChapter.ts"; import IChapter from "./types/IChapter.ts";
export const GetAllManga = async (apiUri: string) : Promise<IManga[]> => { export const GetAllManga = async (apiUri: string) : Promise<IManga[]> => {
@ -15,63 +15,83 @@ export const GetMangaWithIds = async (apiUri: string, mangaIds: string[]) : Prom
export const GetMangaById = async (apiUri: string, mangaId: string) : Promise<IManga> => { export const GetMangaById = async (apiUri: string, mangaId: string) : Promise<IManga> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1) if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided"); return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}`) as Promise<IManga>; return await getData(`${apiUri}/v2/Manga/${mangaId}`) as Promise<IManga>;
} }
export const DeleteManga = async (apiUri: string, mangaId: string) : Promise<void> => { export const DeleteManga = async (apiUri: string, mangaId: string) : Promise<void> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1) if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided"); return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await deleteData(`${apiUri}/v2/Manga/${mangaId}`); return await deleteData(`${apiUri}/v2/Manga/${mangaId}`);
} }
export const GetMangaCoverImageUrl = (apiUri: string, mangaId: string, ref: HTMLImageElement | undefined | null) : string => { export const GetMangaCoverImageUrl = (apiUri: string, mangaId: string, ref: HTMLImageElement | undefined | null) : string => {
if(ref == null || ref == undefined) if(ref == null || ref == undefined)
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=64&height=64`; return `${apiUri}/v2/Manga/${mangaId}/Cover?width=64&height=64`;
if(mangaId === DefaultManga.mangaId)
return "/blahaj.png";
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`; return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`;
} }
export const GetChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => { export const GetChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1) if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided"); return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters`) as Promise<IChapter[]>; return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters`) as Promise<IChapter[]>;
} }
export const GetDownloadedChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => { export const GetDownloadedChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1) if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided"); return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/Downloaded`) as Promise<IChapter[]>; return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/Downloaded`) as Promise<IChapter[]>;
} }
export const GetNotDownloadedChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => { export const GetNotDownloadedChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1) if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided"); return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/NotDownloaded`) as Promise<IChapter[]>; return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/NotDownloaded`) as Promise<IChapter[]>;
} }
export const GetLatestChapterAvailable = async (apiUri: string, mangaId: string) : Promise<IChapter> => { export const GetLatestChapterAvailable = async (apiUri: string, mangaId: string) : Promise<IChapter> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1) if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided"); return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestAvailable`) as Promise<IChapter>; return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestAvailable`) as Promise<IChapter>;
} }
export const GetLatestChapterDownloaded = async (apiUri: string, mangaId: string) : Promise<IChapter> => { export const GetLatestChapterDownloaded = async (apiUri: string, mangaId: string) : Promise<IChapter> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1) if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided"); return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestDownloaded`) as Promise<IChapter>; return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestDownloaded`) as Promise<IChapter>;
} }
export const SetIgnoreThreshold = async (apiUri: string, mangaId: string, chapterThreshold: number) : Promise<object> => { export const SetIgnoreThreshold = async (apiUri: string, mangaId: string, chapterThreshold: number) : Promise<object | undefined> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1) if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided"); return Promise.reject("mangaId was not provided");
if(chapterThreshold === undefined || chapterThreshold === null) if(chapterThreshold === undefined || chapterThreshold === null)
return Promise.reject("chapterThreshold was not provided"); return Promise.reject("chapterThreshold was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await patchData(`${apiUri}/v2/Manga/${mangaId}/IgnoreChaptersBefore`, chapterThreshold); return await patchData(`${apiUri}/v2/Manga/${mangaId}/IgnoreChaptersBefore`, chapterThreshold);
} }
export const MoveFolder = async (apiUri: string, mangaId: string, newPath: string) : Promise<object> => { export const MoveFolder = async (apiUri: string, mangaId: string, newPath: string) : Promise<object | undefined> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1) if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided"); return Promise.reject("mangaId was not provided");
if(newPath === undefined || newPath === null || newPath.length < 1) if(newPath === undefined || newPath === null || newPath.length < 1)
return Promise.reject("newPath was not provided"); return Promise.reject("newPath was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await postData(`${apiUri}/v2/Manga/{MangaId}/MoveFolder`, {newPath}); return await postData(`${apiUri}/v2/Manga/{MangaId}/MoveFolder`, {newPath});
} }

View File

@ -17,7 +17,7 @@ export const GetDisabledConnectors = async (apiUri: string) : Promise<IMangaCon
return await getData(`${apiUri}/v2/MangaConnector/disabled`) as Promise<IMangaConnector[]> return await getData(`${apiUri}/v2/MangaConnector/disabled`) as Promise<IMangaConnector[]>
} }
export const SetConnectorEnabled = async (apiUri: string, connectorName: string, enabled: boolean) : Promise<object> => { export const SetConnectorEnabled = async (apiUri: string, connectorName: string, enabled: boolean) : Promise<object | undefined> => {
if(connectorName === undefined || connectorName === null || connectorName.length < 1) if(connectorName === undefined || connectorName === null || connectorName.length < 1)
return Promise.reject("connectorName was not provided"); return Promise.reject("connectorName was not provided");
if(enabled === undefined || enabled === null) if(enabled === undefined || enabled === null)

View File

@ -2,24 +2,33 @@ import {createContext} from "react";
export const ApiUriContext = createContext<string>(""); export const ApiUriContext = createContext<string>("");
export function getData(uri: string) : Promise<object> { export function getData(uri: string) : Promise<object | undefined> {
return makeRequest("GET", uri, null) as Promise<object>; return makeRequestWrapper("GET", uri, null);
} }
export function postData(uri: string, content: object | string | number | boolean) : Promise<object> { export function postData(uri: string, content: object | string | number | boolean) : Promise<object | undefined> {
return makeRequest("POST", uri, content) as Promise<object>; return makeRequestWrapper("POST", uri, content);
} }
export function deleteData(uri: string) : Promise<void> { export function deleteData(uri: string) : Promise<void> {
return makeRequest("DELETE", uri, null) as Promise<void>; return makeRequestWrapper("DELETE", uri, null) as Promise<void>;
} }
export function patchData(uri: string, content: object | string | number | boolean) : Promise<object> { export function patchData(uri: string, content: object | string | number | boolean) : Promise<object | undefined> {
return makeRequest("patch", uri, content) as Promise<object>; return makeRequestWrapper("patch", uri, content);
} }
export function putData(uri: string, content: object | string | number | boolean) : Promise<object> { export function putData(uri: string, content: object | string | number | boolean) : Promise<object | undefined> {
return makeRequest("PUT", uri, content) as Promise<object>; return makeRequestWrapper("PUT", uri, content);
}
function makeRequestWrapper(method: string, uri: string, content: object | string | number | null | boolean) : Promise<object | undefined>{
return makeRequest(method, uri, content)
.then((result) => result as Promise<object>)
.catch((e) => {
console.warn(e);
return Promise.resolve(undefined);
});
} }
let currentlyRequestedEndpoints: string[] = []; let currentlyRequestedEndpoints: string[] = [];

View File

@ -1,5 +1,5 @@
import IJob from "./IJob"; import IJobWithMangaId from "./IJobWithMangaId.ts";
export default interface IDownloadAvailableChaptersJob extends IJobWithMangaId {
export default interface IDownloadAvailableChaptersJob extends IJob {
mangaId: string;
} }

View File

@ -1,5 +1,5 @@
import IJob from "./IJob"; import IJobWithMangaId from "./IJobWithMangaId.ts";
export default interface IDownloadMangaCoverJob extends IJobWithMangaId {
export default interface IDownloadMangaCoverJob extends IJob {
mangaId: string;
} }

View File

@ -1,5 +1,5 @@
import IJob from "./IJob"; import IJobWithChapterId from "./IJobWithChapterId.tsx";
export default interface IDownloadSingleChapterJob extends IJobWithChapterId {
export default interface IDownloadSingleChapterJob extends IJob {
chapterId: string;
} }

View File

@ -18,7 +18,8 @@ export enum JobType {
RetrieveChaptersJob = "RetrieveChaptersJob", RetrieveChaptersJob = "RetrieveChaptersJob",
UpdateChaptersDownloadedJob = "UpdateChaptersDownloadedJob", UpdateChaptersDownloadedJob = "UpdateChaptersDownloadedJob",
MoveMangaLibraryJob = "MoveMangaLibraryJob", MoveMangaLibraryJob = "MoveMangaLibraryJob",
UpdateSingleChapterDownloadedJob = "UpdateSingleChapterDownloadedJob" UpdateSingleChapterDownloadedJob = "UpdateSingleChapterDownloadedJob",
UpdateCoverJob = "UpdateCoverJob"
} }
export enum JobState { export enum JobState {

View File

@ -0,0 +1,5 @@
import IJob from "./IJob.ts";
export default interface IJobWithChapterId extends IJob {
chapterId: string;
}

View File

@ -0,0 +1,5 @@
import IJob from "./IJob.ts";
export default interface IJobWithMangaId extends IJob {
mangaId: string;
}

View File

@ -1,6 +1,5 @@
import IJob from "./IJob"; import IJobWithMangaId from "./IJobWithMangaId.ts";
export default interface IMoveMangaLibraryJob extends IJob { export default interface IMoveMangaLibraryJob extends IJobWithMangaId {
MangaId: string;
ToLibraryId: string; ToLibraryId: string;
} }

View File

@ -1,5 +1,5 @@
import IJob from "./IJob"; import IJobWithMangaId from "./IJobWithMangaId.ts";
export default interface IRetrieveChaptersJob extends IJobWithMangaId {
export default interface IRetrieveChaptersJob extends IJob {
mangaId: string;
} }

View File

@ -0,0 +1,5 @@
import IJobWithMangaId from "./IJobWithMangaId.ts";
export default interface IUpdateChaptersDownloadedJob extends IJobWithMangaId {
}

View File

@ -0,0 +1,5 @@
import IJobWithMangaId from "./IJobWithMangaId.ts";
export default interface IUpdateCoverJob extends IJobWithMangaId {
}

View File

@ -1,5 +0,0 @@
import IJob from "./IJob";
export default interface IUpdateFilesDownloadedJob extends IJob {
mangaId: string;
}

View File

@ -1,5 +0,0 @@
import IJob from "./IJob";
export default interface IUpdateMetadataJob extends IJob {
mangaId: string;
}

View File

@ -0,0 +1,5 @@
import IJobWithChapterId from "./IJobWithChapterId.tsx";
export default interface IUpdateChaptersDownloadedJob extends IJobWithChapterId {
}