Compare commits

...

4 Commits

Author SHA1 Message Date
11549a07c3 MangaPopup Drawer 2025-05-17 19:25:31 +02:00
79c13ceb1d Fix Manga-Item to new interfaces,
fix Manga-cover
2025-05-17 18:29:21 +02:00
0907031583 API Dependency-Update 2025-05-17 17:59:30 +02:00
00932bf6bd Remove Lunasea 2025-05-16 19:24:00 +02:00
24 changed files with 288 additions and 197 deletions

View File

@ -38,7 +38,7 @@ export default function App () {
<Badge invisible sx={{margin: "8px !important"}}> <Badge invisible sx={{margin: "8px !important"}}>
<Card onClick={() => setShowSearch(true)} sx={{height:"fit-content",width:"fit-content"}}> <Card onClick={() => setShowSearch(true)} sx={{height:"fit-content",width:"fit-content"}}>
<CardCover sx={{margin:"var(--Card-padding)"}}> <CardCover sx={{margin:"var(--Card-padding)"}}>
<img src={"/blahaj.png"} style={{height: CardHeight + "px", width: CardWidth + "px"}} /> <img src={"/blahaj.png"} style={{height: CardHeight + "px", width: CardWidth + 10 + "px"}} />
</CardCover> </CardCover>
<CardCover sx={{ <CardCover sx={{
background: 'rgba(234, 119, 246, 0.14)', background: 'rgba(234, 119, 246, 0.14)',
@ -46,7 +46,7 @@ export default function App () {
webkitBackdropFilter: 'blur(6.9px)', webkitBackdropFilter: 'blur(6.9px)',
}}/> }}/>
<CardContent> <CardContent>
<Box style={{height: CardHeight + "px", width: CardWidth + "px"}} > <Box style={{height: CardHeight + "px", width: CardWidth + 10 + "px"}} >
<Typography level={"h1"}>Search</Typography> <Typography level={"h1"}>Search</Typography>
</Box> </Box>
</CardContent> </CardContent>

View File

@ -1,23 +1,10 @@
import {Chip, ColorPaletteProp, Skeleton} from "@mui/joy"; import {Chip, ColorPaletteProp} from "@mui/joy";
import {useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx";
import IAuthor from "../api/types/IAuthor.ts"; import IAuthor from "../api/types/IAuthor.ts";
import {GetAuthor} from "../api/Query.tsx";
export default function AuthorTag({authorId, color} : { authorId: string | undefined, color?: ColorPaletteProp }) {
const useAuthor = authorId ?? "AuthorId";
const apiUri = useContext(ApiUriContext);
const [author, setAuthor] = useState<IAuthor>();
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
GetAuthor(apiUri, useAuthor).then(setAuthor).finally(() => setLoading(false));
}, [authorId]);
export default function AuthorTag({author, color} : {author: IAuthor, color?: ColorPaletteProp }) {
return ( return (
<Chip variant={"outlined"} size={"md"} color={color??"primary"}> <Chip variant={"outlined"} size={"md"} color={color??"primary"}>
<Skeleton variant={"text"} loading={loading}>{author?.authorName ?? "Load Failed"}</Skeleton> {author.authorName ?? "Load Failed"}
</Chip> </Chip>
); );
} }

View File

@ -1,25 +1,10 @@
import {Chip, Skeleton, Link, ColorPaletteProp} from "@mui/joy"; import {Chip, Link, ColorPaletteProp} from "@mui/joy";
import {useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx";
import {GetLink} from "../api/Query.tsx";
import ILink from "../api/types/ILink.ts"; import ILink from "../api/types/ILink.ts";
export default function LinkTag({linkId, color} : { linkId: string | undefined, color?: ColorPaletteProp }) { export default function LinkTag({link, color} : { link: ILink | undefined, color?: ColorPaletteProp }) {
const useLink = linkId ?? "LinkId";
const apiUri = useContext(ApiUriContext);
const [link, setLink] = useState<ILink>();
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
GetLink(apiUri, useLink).then(setLink).finally(() => setLoading(false));
}, [linkId]);
return ( return (
<Chip variant={"soft"} size={"sm"} color={color??"primary"}> <Chip variant={"soft"} size={"sm"} color={color??"primary"}>
<Skeleton variant={"text"} loading={loading}> <Link sx={{textDecoration:"underline"}} level={"body-xs"} href={link?.linkUrl}>{link?.linkProvider??"Load Failed"}</Link>
<Link sx={{textDecoration:"underline"}} level={"body-xs"} href={link?.linkUrl}>{link?.linkProvider??"Load Failed"}</Link>
</Skeleton>
</Chip> </Chip>
); );
} }

View File

@ -2,25 +2,16 @@ import {
Badge, Badge,
Box, Box,
Card, Card,
CardActions,
CardContent, CardCover, CardContent, CardCover,
Chip, CircularProgress,
Input,
Link, Link,
Skeleton,
Stack,
Typography
} from "@mui/joy"; } from "@mui/joy";
import IManga, {DefaultManga} from "../api/types/IManga.ts"; import IManga, {DefaultManga} 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 {GetLatestChapterAvailable, GetMangaById, GetMangaCoverImageUrl, SetIgnoreThreshold} from "../api/Manga.tsx"; import {GetMangaById, GetMangaCoverImageUrl} from "../api/Manga.tsx";
import {ApiUriContext} from "../api/fetchApi.tsx"; import {ApiUriContext, getData} from "../api/fetchApi.tsx";
import AuthorTag from "./AuthorTag.tsx";
import LinkTag from "./LinkTag.tsx";
import {ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts"; import {ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
import IChapter from "../api/types/IChapter.ts";
import MarkdownPreview from "@uiw/react-markdown-preview";
import {SxProps} from "@mui/joy/styles/types"; import {SxProps} from "@mui/joy/styles/types";
import MangaPopup from "./MangaPopup.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(DefaultManga);
@ -37,49 +28,39 @@ export function MangaFromId({mangaId, children} : { mangaId: string, children?:
loadManga(); loadManga();
}, []); }, []);
return <Manga manga={manga} loading={loading} children={children} /> return (
<>
{loading ? <></> : <Manga manga={manga} children={children} /> }
</>
);
} }
export const CardWidth = 190; export const CardWidth = 190;
export const CardHeight = 300; export const CardHeight = 300;
export function Manga({manga, children, loading} : { manga: IManga | undefined, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined, loading?: boolean}) { export function Manga({manga: manga, children} : { manga: IManga, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
const useManga = manga ?? DefaultManga;
loading = loading ?? false;
const CoverRef = useRef<HTMLImageElement>(null); const CoverRef = useRef<HTMLImageElement>(null);
const apiUri = useContext(ApiUriContext); const apiUri = useContext(ApiUriContext);
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [mangaMaxChapter, setMangaMaxChapter] = useState<IChapter>();
const [maxChapterLoading, setMaxChapterLoading] = useState<boolean>(true);
const LoadMaxChapter = useCallback(() => {
setMaxChapterLoading(true);
GetLatestChapterAvailable(apiUri, useManga.mangaId)
.then(setMangaMaxChapter)
.finally(() => setMaxChapterLoading(false));
}, [useManga, apiUri]);
const [updatingThreshold, setUpdatingThreshold] = useState<boolean>(false);
const updateIgnoreThreshhold = useCallback((value: number) => {
setUpdatingThreshold(true);
SetIgnoreThreshold(apiUri, useManga.mangaId, value).finally(() => setUpdatingThreshold(false));
},[useManga, apiUri])
useEffect(() => { useEffect(() => {
LoadMaxChapter();
LoadMangaCover(); LoadMangaCover();
}, [useManga]); }, [manga]);
const LoadMangaCover = useCallback(() => { const LoadMangaCover = useCallback(() => {
if(CoverRef.current == null) if(CoverRef.current == null)
return; return;
const coverUrl = GetMangaCoverImageUrl(apiUri, useManga.mangaId, CoverRef.current); const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
if(CoverRef.current.src == coverUrl) if(CoverRef.current.src == coverUrl)
return; return;
CoverRef.current.src = GetMangaCoverImageUrl(apiUri, useManga.mangaId, CoverRef.current);
}, [useManga, apiUri]) //Check if we can fetch the image exists (by fetching it), only then update
getData(coverUrl).then(() => {
if(CoverRef.current) CoverRef.current.src = coverUrl;
});
}, [manga, apiUri])
const coverSx : SxProps = { const coverSx : SxProps = {
height: CardHeight + "px", height: CardHeight + "px",
@ -87,12 +68,6 @@ export function Manga({manga, children, loading} : { manga: IManga | undefined,
position: "relative", position: "relative",
} }
const descriptionSx : SxProps = {
height: CardHeight + "px",
width: CardWidth * 2 + "px",
position: "relative"
}
const coverCss : CSSProperties = { const coverCss : CSSProperties = {
maxHeight: "calc("+CardHeight+"px + 2rem)", maxHeight: "calc("+CardHeight+"px + 2rem)",
maxWidth: "calc("+CardWidth+"px + 2rem)", maxWidth: "calc("+CardWidth+"px + 2rem)",
@ -100,20 +75,19 @@ export function Manga({manga, children, loading} : { manga: IManga | undefined,
const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"]; const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"];
const mangaName = useManga.name.length > 30 ? useManga.name.substring(0, 27) + "..." : useManga.name; const mangaName = manga.name.length > 30 ? manga.name.substring(0, 27) + "..." : manga.name;
return ( return (
<Badge sx={{margin:"8px !important"}} badgeContent={useManga.mangaConnectorId} color={ReleaseStatusToPalette(useManga.releaseStatus)} size={"lg"}> <Badge sx={{margin:"8px !important"}} badgeContent={manga.mangaConnectorName} color={ReleaseStatusToPalette(manga.releaseStatus)} size={"lg"}>
<Card sx={{height:"fit-content",width:"fit-content"}} onClick={(e) => { <Card sx={{height:"fit-content",width:"fit-content"}} onClick={(e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if(interactiveElements.find(x => x == target.localName) == undefined) if(interactiveElements.find(x => x == target.localName) == undefined)
setExpanded(!expanded)} setExpanded(!expanded)}
}> }>
<CardCover> <CardCover>
<img style={coverCss} src="/blahaj.png" alt="Manga Cover" <img style={coverCss} src={GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current)} alt="Manga Cover"
ref={CoverRef} ref={CoverRef}
onLoad={LoadMangaCover} onLoad={LoadMangaCover}/>
onResize={LoadMangaCover}/>
</CardCover> </CardCover>
<CardCover sx={{ <CardCover sx={{
background: background:
@ -121,61 +95,12 @@ export function Manga({manga, children, loading} : { manga: IManga | undefined,
}}/> }}/>
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}> <CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
<Box sx={coverSx}> <Box sx={coverSx}>
<Skeleton loading={loading}> <Link href={manga.websiteUrl} level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
<Link href={useManga.websiteUrl} level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}> {mangaName}
{mangaName} </Link>
</Link>
</Skeleton>
</Box> </Box>
{
expanded ?
<Box sx={descriptionSx}>
<Skeleton loading={loading} variant={"text"} level={"title-lg"}>
<Stack direction={"row"} flexWrap={"wrap"} spacing={0.5} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
{useManga.authorIds.map(authorId => <AuthorTag key={authorId} authorId={authorId} color={"success"} />)}
{useManga.tags.map(tag => <Chip key={tag} variant={"soft"} size={"md"} color={"primary"}>{tag}</Chip>)}
{useManga.linkIds.map(linkId => <LinkTag key={linkId} linkId={linkId} color={"warning"} />)}
</Stack>
</Skeleton>
<Skeleton loading={loading} sx={{maxHeight:"300px"}}>
<MarkdownPreview source={useManga.description} style={{backgroundColor: "transparent", color: "black", maxHeight:CardHeight*0.7+"px", overflowY:"auto", marginTop:"10px", scrollbarWidth: "thin"}} />
</Skeleton>
</Box>
: null
}
</CardContent> </CardContent>
{ <MangaPopup manga={manga} open={expanded}>{children}</MangaPopup>
expanded ?
<CardActions sx={{justifyContent:"space-between"}}>
<Skeleton loading={loading} sx={{maxHeight: "30px", maxWidth:"calc(100% - 40px)"}}>
<Input
type={"number"}
placeholder={"0.0"}
startDecorator={
<>
{
updatingThreshold ?
<CircularProgress color={"primary"} size={"sm"} />
: <Typography>Ch.</Typography>
}
</>
}
endDecorator={
<Typography>
<Skeleton loading={maxChapterLoading}>
/{mangaMaxChapter?.chapterNumber??"Load Failed"}
</Skeleton>
</Typography>
}
sx={{width:"min-content"}}
size={"md"}
onChange={(e) => updateIgnoreThreshhold(e.currentTarget.valueAsNumber)}
/>
{children}
</Skeleton>
</CardActions>
: null
}
</Card> </Card>
</Badge> </Badge>
); );

View File

@ -0,0 +1,110 @@
import IManga from "../api/types/IManga.ts";
import {Badge, Box, Chip, CircularProgress, Drawer, Input, Skeleton, Stack, Typography} from "@mui/joy";
import {ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
import {GetLatestChapterAvailable, GetMangaCoverImageUrl, SetIgnoreThreshold} from "../api/Manga.tsx";
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
import AuthorTag from "./AuthorTag.tsx";
import LinkTag from "./LinkTag.tsx";
import MarkdownPreview from "@uiw/react-markdown-preview";
import {CardHeight} from "./Manga.tsx";
import IChapter from "../api/types/IChapter.ts";
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
export default function MangaPopup({manga, open, children} : {manga: IManga | null, open: boolean, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
const apiUri = useContext(ApiUriContext);
const CoverRef = useRef<HTMLImageElement>(null);
const LoadMangaCover = useCallback(() => {
if(CoverRef.current == null || manga == null)
return;
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
if(CoverRef.current.src == coverUrl)
return;
//Check if we can fetch the image exists (by fetching it), only then update
getData(coverUrl).then(() => {
if(CoverRef.current) CoverRef.current.src = coverUrl;
});
}, [manga, apiUri])
useEffect(() => {
if(!open)
return;
LoadMaxChapter();
LoadMangaCover();
}, [open]);
const [mangaMaxChapter, setMangaMaxChapter] = useState<IChapter>();
const [maxChapterLoading, setMaxChapterLoading] = useState<boolean>(true);
const LoadMaxChapter = useCallback(() => {
if(manga == null)
return;
setMaxChapterLoading(true);
GetLatestChapterAvailable(apiUri, manga.mangaId)
.then(setMangaMaxChapter)
.finally(() => setMaxChapterLoading(false));
}, [manga, apiUri]);
const [updatingThreshold, setUpdatingThreshold] = useState<boolean>(false);
const updateIgnoreThreshhold = useCallback((value: number) => {
if(manga == null)
return;
setUpdatingThreshold(true);
SetIgnoreThreshold(apiUri, manga.mangaId, value).finally(() => setUpdatingThreshold(false));
},[manga, apiUri])
return (
<Drawer anchor="bottom" size="lg" open={open}>
<Stack direction="column" spacing={2} margin={"10px"}>
{ /* Cover and Description */ }
<Stack direction="row" spacing={2} margin={"10px"}>
<Badge sx={{margin:"8px !important"}} badgeContent={manga?.mangaConnectorName} color={ReleaseStatusToPalette(manga?.releaseStatus??MangaReleaseStatus.Unreleased)} size={"lg"}>
<img src="/blahaj.png" alt="Manga Cover"
ref={CoverRef}
onLoad={LoadMangaCover}/>
</Badge>
<Box>
<Typography level={"h2"} marginTop={"20px"}>{manga?.name}</Typography>
<Stack direction={"row"} flexWrap={"wrap"} spacing={0.5} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
{manga?.authors?.map(author => <AuthorTag key={author.authorId} author={author} color={"success"} />)}
{manga?.mangaTags?.map(tag => <Chip key={tag.tag} variant={"soft"} size={"md"} color={"primary"}>{tag.tag}</Chip>)}
{manga?.links?.map(link => <LinkTag key={link.linkId} link={link} color={"warning"} />)}
</Stack>
<MarkdownPreview source={manga?.description} style={{backgroundColor: "transparent", color: "var(--joy-palette-neutral-50)", maxHeight:CardHeight*0.7+"px", overflowY:"auto", marginTop:"10px", scrollbarWidth: "thin"}} />
</Box>
</Stack>
{ /* Actions */ }
<Stack direction="row" spacing={2}>
<Input
type={"number"}
placeholder={"0.0"}
startDecorator={
<>
{
updatingThreshold ?
<CircularProgress color={"primary"} size={"sm"} />
: <Typography>Ch.</Typography>
}
</>
}
endDecorator={
<Typography>
<Skeleton loading={maxChapterLoading}>
/{mangaMaxChapter?.chapterNumber??"-"}
</Skeleton>
</Typography>
}
sx={{width:"min-content"}}
size={"md"}
onChange={(e) => updateIgnoreThreshhold(e.currentTarget.valueAsNumber)}
/>
{children}
</Stack>
</Stack>
</Drawer>
);
}

View File

@ -164,7 +164,7 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
})} })}
</Select> </Select>
<Button disabled={localLibrariesLoading || selectedLibraryId === undefined} onClick={() => { <Button disabled={localLibrariesLoading || selectedLibraryId === undefined} onClick={() => {
CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {localLibraryId: selectedLibraryId!,recurrenceTimeMs: 1000 * 60 * 60 * 3}) CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {localLibraryId: selectedLibraryId!,recurrenceTimeMs: 1000 * 60 * 60 * 3, language: "en"})
}} endDecorator={<Add />}>Watch</Button> }} endDecorator={<Add />}>Watch</Button>
</Manga>)} </Manga>)}
</Stack> </Stack>

View File

@ -1,7 +1,7 @@
import {deleteData, getData, patchData, postData, putData} from "./fetchApi"; import {deleteData, getData, patchData, postData, putData} from "./fetchApi";
import IJob, {JobState, JobType} from "./types/Jobs/IJob"; import IJob, {JobState, JobType} from "./types/Jobs/IJob";
import IModifyJobRecord from "./types/records/IModifyJobRecord"; import IModifyJobRecord from "./types/records/IModifyJobRecord";
import IDownloadAvailableJobsRecord from "./types/records/IDownloadAvailableJobsRecord.ts"; import IDownloadAvailableChaptersJobRecord from "./types/records/IDownloadAvailableChaptersJobRecord.ts";
export const GetAllJobs = async (apiUri: string) : Promise<IJob[]> => { export const GetAllJobs = async (apiUri: string) : Promise<IJob[]> => {
return await getData(`${apiUri}/v2/Job`) as Promise<IJob[]>; return await getData(`${apiUri}/v2/Job`) as Promise<IJob[]>;
@ -54,7 +54,7 @@ export const ModifyJob = async (apiUri: string, jobId: string, modifyData: IModi
return await patchData(`${apiUri}/v2/Job/${jobId}`, modifyData) as Promise<IJob>; return await patchData(`${apiUri}/v2/Job/${jobId}`, modifyData) as Promise<IJob>;
} }
export const CreateDownloadAvailableChaptersJob = async (apiUri: string, mangaId: string, data: IDownloadAvailableJobsRecord) : Promise<string[]> => { export const CreateDownloadAvailableChaptersJob = async (apiUri: string, mangaId: string, data: IDownloadAvailableChaptersJobRecord) : Promise<string[]> => {
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(data === undefined || data === null) if(data === undefined || data === null)

View File

@ -24,7 +24,7 @@ export const DeleteManga = async (apiUri: string, mangaId: string) : Promise<voi
return await deleteData(`${apiUri}/v2/Manga/${mangaId}`); return await deleteData(`${apiUri}/v2/Manga/${mangaId}`);
} }
export const GetMangaCoverImageUrl = (apiUri: string, mangaId: string, ref: HTMLImageElement | undefined) : 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`;
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`; return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`;

View File

@ -2,7 +2,6 @@ import {deleteData, getData, putData} from "./fetchApi.tsx";
import INotificationConnector from "./types/INotificationConnector.ts"; import INotificationConnector from "./types/INotificationConnector.ts";
import IGotifyRecord from "./types/records/IGotifyRecord.ts"; import IGotifyRecord from "./types/records/IGotifyRecord.ts";
import INtfyRecord from "./types/records/INtfyRecord.ts"; import INtfyRecord from "./types/records/INtfyRecord.ts";
import ILunaseaRecord from "./types/records/ILunaseaRecord.ts";
import IPushoverRecord from "./types/records/IPushoverRecord.ts"; import IPushoverRecord from "./types/records/IPushoverRecord.ts";
export const GetNotificationConnectors = async (apiUri: string) : Promise<INotificationConnector[]> => { export const GetNotificationConnectors = async (apiUri: string) : Promise<INotificationConnector[]> => {
@ -39,12 +38,6 @@ export const CreateNtfy = async (apiUri: string, ntfy: INtfyRecord) : Promise<s
return await putData(`${apiUri}/v2/NotificationConnector/Ntfy`, ntfy) as Promise<string>; return await putData(`${apiUri}/v2/NotificationConnector/Ntfy`, ntfy) as Promise<string>;
} }
export const CreateLunasea = async (apiUri: string, lunasea: ILunaseaRecord) : Promise<string> => {
if(lunasea === undefined || lunasea === null)
return Promise.reject("lunasea was not provided");
return await putData(`${apiUri}/v2/NotificationConnector/Lunasea`, lunasea) as Promise<string>;
}
export const CreatePushover = async (apiUri: string, pushover: IPushoverRecord) : Promise<string> => { export const CreatePushover = async (apiUri: string, pushover: IPushoverRecord) : Promise<string> => {
if(pushover === undefined || pushover === null) if(pushover === undefined || pushover === null)
return Promise.reject("pushover was not provided"); return Promise.reject("pushover was not provided");

View File

@ -1,4 +1,4 @@
import {postData} from "./fetchApi.tsx"; import {getData, postData} from "./fetchApi.tsx";
import IManga from "./types/IManga.ts"; import IManga from "./types/IManga.ts";
export const SearchName = async (apiUri: string, name: string) : Promise<IManga[]> => { export const SearchName = async (apiUri: string, name: string) : Promise<IManga[]> => {
@ -12,7 +12,7 @@ export const SearchNameOnConnector = async (apiUri: string, connectorName: strin
return Promise.reject("connectorName was not provided"); return Promise.reject("connectorName was not provided");
if(name === undefined || name === null || name.length < 1) if(name === undefined || name === null || name.length < 1)
return Promise.reject("name was not provided"); return Promise.reject("name was not provided");
return await postData(`${apiUri}/v2/Search/${connectorName}`, name) as Promise<IManga[]>; return await getData(`${apiUri}/v2/Search/${connectorName}/${name}`) as Promise<IManga[]>;
} }
export const SearchUrl = async (apiUri: string, url: string) : Promise<IManga> => { export const SearchUrl = async (apiUri: string, url: string) : Promise<IManga> => {

View File

@ -1,4 +0,0 @@
export enum LibraryType {
Komga = "Komga",
Kavita = "Kavita"
}

View File

@ -1,10 +1,11 @@
export default interface IChapter{ export default interface IChapter{
chapterId: string; chapterId: string;
volumeNumber: number; parentMangaId: string;
volumeNumber: number | null;
chapterNumber: string; chapterNumber: string;
url: string; url: string;
title: string | undefined; title: string | null;
archiveFileName: string; fileName: string | null;
downloaded: boolean; downloaded: boolean;
parentMangaId: string; fullArchiveFilePath: string;
} }

View File

@ -1,8 +1,11 @@
import {LibraryType} from "./EnumLibraryType";
export default interface ILibraryConnector { export default interface ILibraryConnector {
libraryConnectorId: string; libraryConnectorId: string;
libraryType: LibraryType; libraryType: LibraryType;
baseUrl: string; baseUrl: string;
auth: string; auth: string;
}
export enum LibraryType {
Komga = "Komga",
Kavita = "Kavita"
} }

View File

@ -1,4 +1,8 @@
import {MangaReleaseStatus} from "./EnumMangaReleaseStatus"; import {MangaReleaseStatus} from "./EnumMangaReleaseStatus";
import IAuthor from "./IAuthor.ts";
import IMangaAltTitle from "./IMangaAltTitle.ts";
import IMangaTag from "./IMangaTag.ts";
import ILink from "./ILink.ts";
export default interface IManga{ export default interface IManga{
mangaId: string; mangaId: string;
@ -6,16 +10,18 @@ export default interface IManga{
name: string; name: string;
description: string; description: string;
websiteUrl: string; websiteUrl: string;
year: number;
originalLanguage: string;
releaseStatus: MangaReleaseStatus; releaseStatus: MangaReleaseStatus;
folderName: string; libraryId: string | null;
ignoreChapterBefore: number; mangaConnectorName: string;
mangaConnectorId: string; authors: IAuthor[] | null;
authorIds: string[]; mangaTags: IMangaTag[] | null;
tags: string[]; links: ILink[] | null;
linkIds: string[]; altTitles: IMangaAltTitle[] | null;
altTitleIds: string[]; ignoreChaptersBefore: number;
directoryName: string;
year: number | null;
originalLanguage: string | null;
chapterIds: string[] | null;
} }
export const DefaultManga : IManga = { export const DefaultManga : IManga = {
@ -24,14 +30,16 @@ export const DefaultManga : IManga = {
name: "Loading", name: "Loading",
description: "Loading", description: "Loading",
websiteUrl: "", websiteUrl: "",
releaseStatus: MangaReleaseStatus.Continuing,
libraryId: null,
mangaConnectorName: "Loading",
authors: null,
mangaTags: null,
links: null,
altTitles: null,
ignoreChaptersBefore: 0,
directoryName: "",
year: 1999, year: 1999,
originalLanguage: "en", originalLanguage: "en",
releaseStatus: MangaReleaseStatus.Continuing, chapterIds: null
folderName: "Loading",
ignoreChapterBefore: 0,
mangaConnectorId: "Loading",
authorIds: ["Loading"],
tags: ["Loading"],
linkIds: ["Loading"],
altTitleIds: ["Loading"],
} }

View File

@ -0,0 +1,3 @@
export default interface IMangaTag {
tag: string;
}

View File

@ -1,7 +1,6 @@
export default interface IJob{ export default interface IJob{
jobId: string; jobId: string;
parentJobId: string; parentJobId: string | null;
dependsOnJobIds: string[];
jobType: JobType; jobType: JobType;
recurrenceMs: number; recurrenceMs: number;
lastExecution: Date; lastExecution: Date;
@ -17,13 +16,15 @@ export enum JobType {
MoveFileOrFolderJob = "MoveFileOrFolderJob", MoveFileOrFolderJob = "MoveFileOrFolderJob",
DownloadMangaCoverJob = "DownloadMangaCoverJob", DownloadMangaCoverJob = "DownloadMangaCoverJob",
RetrieveChaptersJob = "RetrieveChaptersJob", RetrieveChaptersJob = "RetrieveChaptersJob",
UpdateFilesDownloadedJob = "UpdateFilesDownloadedJob", UpdateChaptersDownloadedJob = "UpdateChaptersDownloadedJob",
MoveMangaLibraryJob = "MoveMangaLibraryJob" MoveMangaLibraryJob = "MoveMangaLibraryJob",
UpdateSingleChapterDownloadedJob = "UpdateSingleChapterDownloadedJob"
} }
export enum JobState { export enum JobState {
Waiting = "Waiting", FirstExecution = "FirstExecution",
Running = "Running", Running = "Running",
Completed = "Completed", Completed = "Completed",
CompletedWaiting = "CompletedWaiting",
Failed = "Failed" Failed = "Failed"
} }

View File

@ -0,0 +1,5 @@
export default interface IDownloadAvailableChaptersJobRecord {
language: string;
recurrenceTimeMs: number;
localLibraryId: string;
}

View File

@ -1,4 +0,0 @@
export default interface IDownloadAvailableJobsRecord {
recurrenceTimeMs: number;
localLibraryId: string;
}

View File

@ -1,5 +1,3 @@
import "../../../styles/notificationConnector.css";
export default interface IGotifyRecord { export default interface IGotifyRecord {
endpoint: string; endpoint: string;
appToken: string; appToken: string;

View File

@ -1,5 +0,0 @@
import "../../../styles/notificationConnector.css";
export default interface ILunaseaRecord {
id: string;
}

View File

@ -1,5 +1,3 @@
import "../../../styles/notificationConnector.css";
export default interface INtfyRecord { export default interface INtfyRecord {
endpoint: string; endpoint: string;
username: string; username: string;

View File

@ -1,5 +1,3 @@
import "../../../styles/notificationConnector.css";
export default interface IPushoverRecord { export default interface IPushoverRecord {
apptoken: string; apptoken: string;
user: string; user: string;

View File

@ -6,11 +6,12 @@ import '@fontsource/inter';
import { CssVarsProvider } from '@mui/joy/styles'; import { CssVarsProvider } from '@mui/joy/styles';
import CssBaseline from '@mui/joy/CssBaseline'; import CssBaseline from '@mui/joy/CssBaseline';
import {StrictMode} from "react"; import {StrictMode} from "react";
import {trangaTheme} from "./theme.ts";
export default function MyApp() { export default function MyApp() {
return ( return (
<StrictMode> <StrictMode>
<CssVarsProvider> <CssVarsProvider theme={trangaTheme}>
{/* must be used under CssVarsProvider */} {/* must be used under CssVarsProvider */}
<CssBaseline /> <CssBaseline />

View File

@ -0,0 +1,88 @@
import { extendTheme } from '@mui/joy/styles';
export const trangaTheme = extendTheme({
"colorSchemes": {
"light": {
"palette": {
"primary": {
"50": "#FCE5EA",
"100": "#FBDDE3",
"200": "#F9CBD4",
"300": "#F7BAC6",
"400": "#F5A9B8",
"500": "#F5A9B8",
"600": "#C48793",
"700": "#AC7681",
"800": "#93656E",
"900": "#7B555C"
},
"neutral": {
"50": "#E6E6E6",
"100": "#CCCCCC",
"200": "#B3B3B3",
"300": "#999999",
"400": "#808080",
"500": "#666666",
"600": "#4C4C4C",
"700": "#333333",
"800": "#191919",
"900": "#000",
"plainColor": "var(--joy-palette-neutral-50)",
"plainHoverBg": "var(--joy-palette-neutral-700)",
"outlinedColor": "var(--joy-palette-neutral-50)",
},
"success": {
"50": "#cef0fe",
"100": "#bdebfd",
"200": "#9de2fc",
"300": "#7cd8fb",
"400": "#5bcefa",
"500": "#5bcefa",
"600": "#49a5c8",
"700": "#4090af",
"800": "#2e677d",
"900": "#245264"
},
"danger": {
"50": "#f2c0b3",
"100": "#ea9680",
"200": "#e68166",
"300": "#dd5733",
"400": "#d52d00",
"500": "#d52d00",
"600": "#aa2400",
"700": "#951f00",
"800": "#6b1700",
"900": "#400d00"
},
"warning": {
"50": "#ffebdd",
"100": "#ffd7bb",
"200": "#ffc29a",
"300": "#ffae78",
"400": "#ff9a56",
"500": "#ff9a56",
"600": "#cc7b45",
"700": "#995c34",
"800": "#663e22",
"900": "#331f11"
},
"background": {
"body": "var(--joy-palette-neutral-900)",
"surface": "var(--joy-palette-neutral-900)",
"popup": "var(--joy-palette-neutral-800)"
},
"text": {
"primary": "var(--joy-palette-neutral-50)",
"secondary": "var(--joy-palette-success-200)",
"tertiary": "var(--joy-palette-primary-200)",
"icon": "var(--joy-palette-primary-50)"
}
}
},
"dark": {
"palette": {}
}
}
})