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 Settings from "./Settings.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 {ApiUriContext} from "./api/fetchApi.tsx";
import Search from './Components/Search.tsx';
import MangaList from "./Components/MangaList.tsx";
import {CardHeight, CardWidth} from "./Components/Manga.tsx";
import {MangaConnectorContext} from "./api/Contexts/MangaConnectorContext.tsx";
import IMangaConnector from "./api/types/IMangaConnector.ts";
import {GetAllConnectors} from "./api/MangaConnector.tsx";
@ -45,25 +44,7 @@ export default function App () {
<Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri} setConnected={setApiConnected} />
<Search open={showSearch} setOpen={setShowSearch} />
<Sheet className={"app-content"}>
<MangaList connected={apiConnected}>
<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>
<MangaList connected={apiConnected} setShowSearch={setShowSearch} />
</Sheet>
</Sheet>
</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 {
Badge,
Box,
Card,
CardContent, CardCover,
Link,
} from "@mui/joy";
import {Badge, Box, Card, CardContent, CardCover, Skeleton, Typography,} from "@mui/joy";
import IManga from "../api/types/IManga.ts";
import {CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
import {GetMangaById, GetMangaCoverImageUrl} from "../api/Manga.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 MangaPopup from "./MangaPopup.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 }){
const [manga, setManga] = useState<IManga>();
@ -29,14 +37,33 @@ export function MangaFromId({mangaId, children} : { mangaId: string, children?:
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}) {
const CoverRef = useRef<HTMLImageElement>(null);
@ -60,18 +87,7 @@ export function Manga({manga: manga, children} : { manga: IManga, children?: Rea
getData(coverUrl).then(() => {
if(CoverRef.current) CoverRef.current.src = coverUrl;
});
}, [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)",
}
}, [manga, apiUri]);
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"}}>
<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}
</Link>
</Typography>
</Box>
</CardContent>
<MangaPopup manga={manga} open={expanded}>{children}</MangaPopup>

View File

@ -1,14 +1,14 @@
import {Button, Stack} from "@mui/joy";
import {useCallback, useContext, useEffect, useState} from "react";
import {Badge, Box, Button, Card, CardContent, CardCover, Stack, Tooltip, Typography} from "@mui/joy";
import {Dispatch, SetStateAction, useCallback, useContext, useEffect, useState} from "react";
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 IDownloadAvailableChaptersJob from "../api/types/Jobs/IDownloadAvailableChaptersJob.ts";
import {MangaFromId} from "./Manga.tsx";
import { Remove } from "@mui/icons-material";
import {CardHeight, CardWidth, MangaFromId} from "./Manga.tsx";
import {PlayArrow, Remove} from "@mui/icons-material";
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 [jobList, setJobList] = useState<IDownloadAvailableChaptersJob[]>([]);
@ -23,6 +23,10 @@ export default function MangaList({connected, children}: {connected: boolean, ch
DeleteJob(apiUri, jobId).finally(() => getJobList());
},[apiUri]);
const startJob = useCallback((jobId: string) => {
StartJob(apiUri, jobId, true).finally(() => getJobList());
},[apiUri]);
useEffect(() => {
getJobList();
}, [apiUri]);
@ -50,9 +54,28 @@ export default function MangaList({connected, children}: {connected: boolean, ch
return(
<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) => (
<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>
</MangaFromId>
))}

View File

@ -1,10 +1,13 @@
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 {GetLatestChapterAvailable, GetMangaCoverImageUrl, SetIgnoreThreshold} from "../api/Manga.tsx";
import {
GetLatestChapterAvailable,
GetLatestChapterDownloaded,
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";
@ -35,6 +38,7 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
if(!open)
return;
LoadMaxChapter();
LoadDownloadedChapter();
LoadMangaCover();
}, [open]);
@ -49,6 +53,17 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
.finally(() => setMaxChapterLoading(false));
}, [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 updateIgnoreThreshhold = useCallback((value: number) => {
if(manga == null)
@ -70,11 +85,17 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
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"} />)}
<Link href={manga?.websiteUrl} level={"h2"}>
{manga?.name}
</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?.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>
<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>
@ -84,7 +105,7 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
<Stack direction="row" spacing={2}>
<Input
type={"number"}
placeholder={"0.0"}
placeholder={downloadedChapterLoading ? "" : mangaDownloadedChapter?.chapterNumber??"0.0"}
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[]>;
}
export const StartJob = async (apiUri: string, jobId: string) : Promise<object | undefined> => {
return await postData(`${apiUri}/v2/Job/${jobId}/Start`, {});
export const StartJob = async (apiUri: string, jobId: string, startDependencies: boolean) : Promise<object | undefined> => {
return await postData(`${apiUri}/v2/Job/${jobId}/Start`, startDependencies);
}
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);
}
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);
}
@ -22,7 +22,7 @@ export function putData(uri: string, content: object | string | number | boolean
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)
.then((result) => result as Promise<object>)
.catch((e) => {
@ -32,7 +32,7 @@ function makeRequestWrapper(method: string, uri: string, content: object | strin
}
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;
if(currentlyRequestedEndpoints.find(x => x == id) != undefined)
return Promise.reject(`Already requested: ${method} ${uri}`);