Compare commits

...

8 Commits

8 changed files with 110 additions and 89 deletions

View File

@ -2,12 +2,11 @@ import Sheet from '@mui/joy/Sheet';
import './App.css' import './App.css'
import Settings from "./Settings.tsx"; import Settings from "./Settings.tsx";
import Header from "./Header.tsx"; import Header from "./Header.tsx";
import {Badge, Box, Button, Card, CardContent, CardCover, Typography} from "@mui/joy"; import {Badge, Button} from "@mui/joy";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {ApiUriContext} from "./api/fetchApi.tsx"; import {ApiUriContext} from "./api/fetchApi.tsx";
import Search from './Components/Search.tsx'; import Search from './Components/Search.tsx';
import MangaList from "./Components/MangaList.tsx"; import MangaList from "./Components/MangaList.tsx";
import {CardHeight, CardWidth} from "./Components/Manga.tsx";
import {MangaConnectorContext} from "./api/Contexts/MangaConnectorContext.tsx"; import {MangaConnectorContext} from "./api/Contexts/MangaConnectorContext.tsx";
import IMangaConnector from "./api/types/IMangaConnector.ts"; import IMangaConnector from "./api/types/IMangaConnector.ts";
import {GetAllConnectors} from "./api/MangaConnector.tsx"; import {GetAllConnectors} from "./api/MangaConnector.tsx";
@ -45,25 +44,7 @@ export default function App () {
<Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri} setConnected={setApiConnected} /> <Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri} setConnected={setApiConnected} />
<Search open={showSearch} setOpen={setShowSearch} /> <Search open={showSearch} setOpen={setShowSearch} />
<Sheet className={"app-content"}> <Sheet className={"app-content"}>
<MangaList connected={apiConnected}> <MangaList connected={apiConnected} setShowSearch={setShowSearch} />
<Badge invisible sx={{margin: "8px !important"}}>
<Card onClick={() => setShowSearch(true)} sx={{height:"fit-content",width:"fit-content"}}>
<CardCover sx={{margin:"var(--Card-padding)"}}>
<img src={"/blahaj.png"} style={{height: CardHeight + "px", width: CardWidth + 10 + "px"}} />
</CardCover>
<CardCover sx={{
background: 'rgba(234, 119, 246, 0.14)',
backdropFilter: 'blur(6.9px)',
webkitBackdropFilter: 'blur(6.9px)',
}}/>
<CardContent>
<Box style={{height: CardHeight + "px", width: CardWidth + 10 + "px"}} >
<Typography level={"h1"}>Search</Typography>
</Box>
</CardContent>
</Card>
</Badge>
</MangaList>
</Sheet> </Sheet>
</Sheet> </Sheet>
</MangaConnectorContext> </MangaConnectorContext>

View File

@ -1,10 +0,0 @@
import {Chip, ColorPaletteProp} from "@mui/joy";
import IAuthor from "../api/types/IAuthor.ts";
export default function AuthorTag({author, color} : {author: IAuthor, color?: ColorPaletteProp }) {
return (
<Chip variant={"outlined"} size={"md"} color={color??"primary"}>
{author.authorName ?? "Load Failed"}
</Chip>
);
}

View File

@ -1,10 +0,0 @@
import {Chip, Link, ColorPaletteProp} from "@mui/joy";
import ILink from "../api/types/ILink.ts";
export default function LinkTag({link, color} : { link: ILink | undefined, color?: ColorPaletteProp }) {
return (
<Chip variant={"soft"} size={"sm"} color={color??"primary"}>
<Link sx={{textDecoration:"underline"}} level={"body-xs"} href={link?.linkUrl}>{link?.linkProvider??"Load Failed"}</Link>
</Chip>
);
}

View File

@ -1,19 +1,27 @@
import { import {Badge, Box, Card, CardContent, CardCover, Skeleton, Typography,} from "@mui/joy";
Badge,
Box,
Card,
CardContent, CardCover,
Link,
} from "@mui/joy";
import IManga 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";
import {ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts"; import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
import {SxProps} from "@mui/joy/styles/types"; import {SxProps} from "@mui/joy/styles/types";
import MangaPopup from "./MangaPopup.tsx"; import MangaPopup from "./MangaPopup.tsx";
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx"; import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx";
export const CardWidth = 190;
export const CardHeight = 300;
const coverSx : SxProps = {
height: CardHeight + "px",
width: CardWidth + "px",
position: "relative",
}
const coverCss : CSSProperties = {
maxHeight: "calc("+CardHeight+"px + 2rem)",
maxWidth: "calc("+CardWidth+"px + 2rem)",
}
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<IManga>(); const [manga, setManga] = useState<IManga>();
@ -29,14 +37,33 @@ export function MangaFromId({mangaId, children} : { mangaId: string, children?:
return ( return (
<> <>
{manga === undefined ? <></> : <Manga manga={manga} children={children} /> } {manga === undefined ?
<Badge sx={{margin:"8px !important"}} badgeContent={<Skeleton><img width={"24pt"} height={"24pt"} src={"/blahaj.png"} /></Skeleton>} color={ReleaseStatusToPalette(MangaReleaseStatus.Completed)} size={"lg"}>
<Card sx={{height:"fit-content",width:"fit-content"}}>
<CardCover>
<img style={coverCss} src={"/blahaj.png"} alt="Manga Cover"/>
</CardCover>
<CardCover sx={{
background:
'linear-gradient(to bottom, rgba(0,0,0,0.4), rgba(0,0,0,0) 200px), linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0) 300px)',
}}/>
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
<Box sx={coverSx}>
<Typography level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
<Skeleton loading={true} animation={"wave"}>
{"x ".repeat(Math.random()*25+5)}
</Skeleton>
</Typography>
</Box>
</CardContent>
</Card>
</Badge>
:
<Manga manga={manga} children={children} /> }
</> </>
); );
} }
export const CardWidth = 190;
export const CardHeight = 300;
export function Manga({manga: manga, children} : { manga: IManga, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) { export function Manga({manga: manga, children} : { manga: IManga, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
const CoverRef = useRef<HTMLImageElement>(null); const CoverRef = useRef<HTMLImageElement>(null);
@ -60,18 +87,7 @@ export function Manga({manga: manga, children} : { manga: IManga, children?: Rea
getData(coverUrl).then(() => { getData(coverUrl).then(() => {
if(CoverRef.current) CoverRef.current.src = coverUrl; if(CoverRef.current) CoverRef.current.src = coverUrl;
}); });
}, [manga, apiUri]) }, [manga, apiUri]);
const coverSx : SxProps = {
height: CardHeight + "px",
width: CardWidth + "px",
position: "relative",
}
const coverCss : CSSProperties = {
maxHeight: "calc("+CardHeight+"px + 2rem)",
maxWidth: "calc("+CardWidth+"px + 2rem)",
}
const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"]; const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"];
@ -95,9 +111,9 @@ export function Manga({manga: manga, children} : { manga: IManga, children?: Rea
}}/> }}/>
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}> <CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
<Box sx={coverSx}> <Box sx={coverSx}>
<Link href={manga.websiteUrl} level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}> <Typography level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
{mangaName} {mangaName}
</Link> </Typography>
</Box> </Box>
</CardContent> </CardContent>
<MangaPopup manga={manga} open={expanded}>{children}</MangaPopup> <MangaPopup manga={manga} open={expanded}>{children}</MangaPopup>

View File

@ -1,14 +1,14 @@
import {Button, Stack} from "@mui/joy"; import {Badge, Box, Button, Card, CardContent, CardCover, Stack, Tooltip, Typography} from "@mui/joy";
import {useCallback, useContext, useEffect, useState} from "react"; import {Dispatch, SetStateAction, useCallback, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx"; import {ApiUriContext} from "../api/fetchApi.tsx";
import {DeleteJob, GetJobsWithType} from "../api/Job.tsx"; import {DeleteJob, GetJobsWithType, StartJob} from "../api/Job.tsx";
import {JobType} from "../api/types/Jobs/IJob.ts"; import {JobType} from "../api/types/Jobs/IJob.ts";
import IDownloadAvailableChaptersJob from "../api/types/Jobs/IDownloadAvailableChaptersJob.ts"; import IDownloadAvailableChaptersJob from "../api/types/Jobs/IDownloadAvailableChaptersJob.ts";
import {MangaFromId} from "./Manga.tsx"; import {CardHeight, CardWidth, MangaFromId} from "./Manga.tsx";
import { Remove } from "@mui/icons-material"; import {PlayArrow, Remove} from "@mui/icons-material";
import * as React from "react"; import * as React from "react";
export default function MangaList({connected, children}: {connected: boolean, children?: React.ReactNode} ){ export default function MangaList({connected, setShowSearch}: {connected: boolean, setShowSearch: Dispatch<SetStateAction<boolean>>} ) {
const apiUri = useContext(ApiUriContext); const apiUri = useContext(ApiUriContext);
const [jobList, setJobList] = useState<IDownloadAvailableChaptersJob[]>([]); const [jobList, setJobList] = useState<IDownloadAvailableChaptersJob[]>([]);
@ -23,6 +23,10 @@ export default function MangaList({connected, children}: {connected: boolean, ch
DeleteJob(apiUri, jobId).finally(() => getJobList()); DeleteJob(apiUri, jobId).finally(() => getJobList());
},[apiUri]); },[apiUri]);
const startJob = useCallback((jobId: string) => {
StartJob(apiUri, jobId, true).finally(() => getJobList());
},[apiUri]);
useEffect(() => { useEffect(() => {
getJobList(); getJobList();
}, [apiUri]); }, [apiUri]);
@ -50,9 +54,28 @@ export default function MangaList({connected, children}: {connected: boolean, ch
return( return(
<Stack direction="row" spacing={1} flexWrap={"wrap"}> <Stack direction="row" spacing={1} flexWrap={"wrap"}>
{children} <Badge invisible sx={{margin: "8px !important"}}>
<Card onClick={() => setShowSearch(true)} sx={{height:"fit-content",width:"fit-content"}}>
<CardCover sx={{margin:"var(--Card-padding)"}}>
<img src={"/blahaj.png"} style={{height: CardHeight + "px", width: CardWidth + 10 + "px"}} />
</CardCover>
<CardCover sx={{
background: 'rgba(234, 119, 246, 0.14)',
backdropFilter: 'blur(6.9px)',
webkitBackdropFilter: 'blur(6.9px)',
}}/>
<CardContent>
<Box style={{height: CardHeight + "px", width: CardWidth + 10 + "px"}} >
<Typography level={"h1"}>Search</Typography>
</Box>
</CardContent>
</Card>
</Badge>
{jobList?.map((job) => ( {jobList?.map((job) => (
<MangaFromId key={job.mangaId} mangaId={job.mangaId}> <MangaFromId key={job.mangaId} mangaId={job.mangaId}>
<Tooltip title={"Last run: " + new Date(job.lastExecution).toLocaleString()}>
<Button color={"success"} endDecorator={<PlayArrow />} onClick={() => startJob(job.jobId)}>Start</Button>
</Tooltip>
<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

@ -1,10 +1,13 @@
import IManga from "../api/types/IManga.ts"; import IManga from "../api/types/IManga.ts";
import {Badge, Box, Chip, CircularProgress, Drawer, Input, Skeleton, Stack, Typography} from "@mui/joy"; import {Badge, Box, Chip, CircularProgress, Drawer, Input, Link, Skeleton, Stack, Typography} from "@mui/joy";
import {ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react"; import {ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
import {GetLatestChapterAvailable, GetMangaCoverImageUrl, SetIgnoreThreshold} from "../api/Manga.tsx"; import {
GetLatestChapterAvailable,
GetLatestChapterDownloaded,
GetMangaCoverImageUrl,
SetIgnoreThreshold
} from "../api/Manga.tsx";
import {ApiUriContext, getData} from "../api/fetchApi.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 MarkdownPreview from "@uiw/react-markdown-preview";
import {CardHeight} from "./Manga.tsx"; import {CardHeight} from "./Manga.tsx";
import IChapter from "../api/types/IChapter.ts"; import IChapter from "../api/types/IChapter.ts";
@ -35,6 +38,7 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
if(!open) if(!open)
return; return;
LoadMaxChapter(); LoadMaxChapter();
LoadDownloadedChapter();
LoadMangaCover(); LoadMangaCover();
}, [open]); }, [open]);
@ -49,6 +53,17 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
.finally(() => setMaxChapterLoading(false)); .finally(() => setMaxChapterLoading(false));
}, [manga, apiUri]); }, [manga, apiUri]);
const [mangaDownloadedChapter, setMangaDownloadedChapter] = useState<IChapter>();
const [downloadedChapterLoading, setDownloadedChapterLoading] = useState<boolean>(true);
const LoadDownloadedChapter = useCallback(() => {
if(manga == null)
return;
setDownloadedChapterLoading(true);
GetLatestChapterDownloaded(apiUri, manga.mangaId)
.then(setMangaDownloadedChapter)
.finally(() => setDownloadedChapterLoading(false));
}, [manga, apiUri]);
const [updatingThreshold, setUpdatingThreshold] = useState<boolean>(false); const [updatingThreshold, setUpdatingThreshold] = useState<boolean>(false);
const updateIgnoreThreshhold = useCallback((value: number) => { const updateIgnoreThreshhold = useCallback((value: number) => {
if(manga == null) if(manga == null)
@ -70,11 +85,17 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
onLoad={LoadMangaCover}/> onLoad={LoadMangaCover}/>
</Badge> </Badge>
<Box> <Box>
<Typography level={"h2"} marginTop={"20px"}>{manga?.name}</Typography> <Link href={manga?.websiteUrl} level={"h2"}>
<Stack direction={"row"} flexWrap={"wrap"} spacing={0.5} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}> {manga?.name}
{manga?.authors?.map(author => <AuthorTag key={author.authorId} author={author} color={"success"} />)} </Link>
<Stack direction={"row"} flexWrap={"wrap"} useFlexGap={true} spacing={0.3} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
{manga?.authors?.map(author => <Chip key={author.authorId} variant={"outlined"} size={"md"} color={"success"}>{author.authorName}</Chip>)}
{manga?.mangaTags?.map(tag => <Chip key={tag.tag} variant={"soft"} size={"md"} color={"primary"}>{tag.tag}</Chip>)} {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"} />)} {manga?.links?.map(link =>
<Chip key={link.linkId} variant={"soft"} size={"md"} color={"warning"}>
<Link sx={{textDecoration:"underline"}} level={"body-xs"} href={link?.linkUrl}>{link?.linkProvider??"Load Failed"}</Link>
</Chip>
)}
</Stack> </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"}} /> <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> </Box>
@ -84,7 +105,7 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
<Stack direction="row" spacing={2}> <Stack direction="row" spacing={2}>
<Input <Input
type={"number"} type={"number"}
placeholder={"0.0"} placeholder={downloadedChapterLoading ? "" : mangaDownloadedChapter?.chapterNumber??"0.0"}
startDecorator={ startDecorator={
<> <>
{ {

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 | undefined> => { export const StartJob = async (apiUri: string, jobId: string, startDependencies: boolean) : Promise<object | undefined> => {
return await postData(`${apiUri}/v2/Job/${jobId}/Start`, {}); return await postData(`${apiUri}/v2/Job/${jobId}/Start`, startDependencies);
} }
export const StopJob = async (apiUri: string, jobId: string) : Promise<object | undefined> => { 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

@ -6,7 +6,7 @@ export function getData(uri: string) : Promise<object | undefined> {
return makeRequestWrapper("GET", uri, null); return makeRequestWrapper("GET", uri, null);
} }
export function postData(uri: string, content: object | string | number | boolean) : Promise<object | undefined> { export function postData(uri: string, content?: object | string | number | boolean | null) : Promise<object | undefined> {
return makeRequestWrapper("POST", uri, content); return makeRequestWrapper("POST", uri, content);
} }
@ -22,7 +22,7 @@ export function putData(uri: string, content: object | string | number | boolean
return makeRequestWrapper("PUT", uri, content); return makeRequestWrapper("PUT", uri, content);
} }
function makeRequestWrapper(method: string, uri: string, content: object | string | number | null | boolean) : Promise<object | undefined>{ function makeRequestWrapper(method: string, uri: string, content?: object | string | number | null | boolean) : Promise<object | undefined>{
return makeRequest(method, uri, content) return makeRequest(method, uri, content)
.then((result) => result as Promise<object>) .then((result) => result as Promise<object>)
.catch((e) => { .catch((e) => {
@ -32,7 +32,7 @@ function makeRequestWrapper(method: string, uri: string, content: object | strin
} }
let currentlyRequestedEndpoints: string[] = []; let currentlyRequestedEndpoints: string[] = [];
function makeRequest(method: string, uri: string, content: object | string | number | null | boolean) : Promise<object | void> { function makeRequest(method: string, uri: string, content?: object | string | number | null | boolean) : Promise<object | void> {
const id = method + uri; const id = method + uri;
if(currentlyRequestedEndpoints.find(x => x == id) != undefined) if(currentlyRequestedEndpoints.find(x => x == id) != undefined)
return Promise.reject(`Already requested: ${method} ${uri}`); return Promise.reject(`Already requested: ${method} ${uri}`);