Jobs List

This commit is contained in:
glax 2025-05-20 03:12:51 +02:00
parent 7cdc31fedb
commit a6d2de35cf
5 changed files with 155 additions and 29 deletions

View File

@ -10,21 +10,69 @@ import MangaList from "./Components/MangaList.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";
import JobsDrawer from "./Components/Jobs.tsx";
import {MangaContext} from "./api/Contexts/MangaContext.tsx";
import IManga from "./api/types/IManga.ts";
import {GetMangaById} from "./api/Manga.tsx";
import IChapter from "./api/types/IChapter.ts";
import {GetChapterFromId} from "./api/Chapter.tsx";
import {ChapterContext} from "./api/Contexts/ChapterContext.tsx";
export default function App () { export default function App () {
const [showSettings, setShowSettings] = useState<boolean>(false); const [showSettings, setShowSettings] = useState<boolean>(false);
const [showSearch, setShowSearch] = useState<boolean>(false); const [showSearch, setShowSearch] = useState<boolean>(false);
const [showJobs, setShowJobs] = useState<boolean>(false);
const [apiConnected, setApiConnected] = useState<boolean>(false); const [apiConnected, setApiConnected] = useState<boolean>(false);
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")); const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/"));
const [apiUri, setApiUri] = useState<string>(apiUriStr); const [apiUri, setApiUri] = useState<string>(apiUriStr);
const [mangas, setMangas] = useState<IManga[]>([]);
const [chapters, setChapters] = useState<IChapter[]>([]);
useEffect(() => { useEffect(() => {
localStorage.setItem("apiUri", apiUri); localStorage.setItem("apiUri", apiUri);
}, [apiUri]); }, [apiUri]);
const [mangaPromises, setMangaPromises] = useState(new Map<string, Promise<IManga | undefined>>());
const GetManga = (mangaId: string) : Promise<IManga | undefined> => {
const promise = mangaPromises.get(mangaId);
if(promise) return promise;
const p = new Promise<IManga | undefined>((resolve, reject) => {
let ret = mangas?.find(m => m.mangaId == mangaId);
if (ret) resolve(ret);
console.log(`Fetching manga ${mangaId}`);
GetMangaById(apiUri, mangaId).then(manga => {
if(manga && mangas?.find(m => m.mangaId == mangaId) === undefined)
setMangas([...mangas, manga]);
resolve(manga);
}).catch(reject);
});
setMangaPromises(mangaPromises.set(mangaId, p));
return p;
}
const [chapterPromises, setChapterPromises] = useState(new Map<string, Promise<IChapter | undefined>>());
const GetChapter = (chapterId: string) : Promise<IChapter | undefined> => {
const promise = chapterPromises.get(chapterId);
if(promise) return promise;
const p = new Promise<IChapter | undefined>((resolve, reject) => {
let ret = chapters?.find(c => c.chapterId == chapterId);
if (ret) resolve(ret);
console.log(`Fetching chapter ${chapterId}`);
GetChapterFromId(apiUri, chapterId).then(chapter => {
if(chapter && chapters?.find(c => c.chapterId == chapterId) === undefined)
setChapters([...chapters, chapter]);
resolve(chapter);
}).catch(reject);
});
setChapterPromises(chapterPromises.set(chapterId, p));
return p;
}
const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>([]); const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>([]);
useEffect(() => { useEffect(() => {
@ -35,18 +83,24 @@ export default function App () {
return ( return (
<ApiUriContext.Provider value={apiUri}> <ApiUriContext.Provider value={apiUri}>
<MangaConnectorContext value={mangaConnectors}> <MangaConnectorContext value={mangaConnectors}>
<MangaContext.Provider value={{mangas, GetManga}}>
<ChapterContext.Provider value={{chapters, GetChapter}}>
<Sheet className={"app"}> <Sheet className={"app"}>
<Header> <Header>
<Badge color={"danger"} invisible={apiConnected} badgeContent={"!"}> <Badge color={"danger"} invisible={apiConnected} badgeContent={"!"}>
<Button onClick={() => setShowSettings(true)}>Settings</Button> <Button onClick={() => setShowSettings(true)}>Settings</Button>
<Button onClick={() => setShowJobs(true)}>Jobs</Button>
</Badge> </Badge>
</Header> </Header>
<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} />
<JobsDrawer open={showJobs} connected={apiConnected} setOpen={setShowJobs} />
<Sheet className={"app-content"}> <Sheet className={"app-content"}>
<MangaList connected={apiConnected} setShowSearch={setShowSearch} /> <MangaList connected={apiConnected} setShowSearch={setShowSearch} />
</Sheet> </Sheet>
</Sheet> </Sheet>
</ChapterContext.Provider>
</MangaContext.Provider>
</MangaConnectorContext> </MangaConnectorContext>
</ApiUriContext.Provider> </ApiUriContext.Provider>
); );

View File

@ -0,0 +1,61 @@
import {ReactElement, useContext, useState} from "react";
import IChapter from "../api/types/IChapter.ts";
import {Card, CardActions, CardContent, Chip, Link, Stack, Tooltip, Typography} from "@mui/joy";
import {MangaFromId} from "./Manga.tsx";
import {ChapterContext} from "../api/Contexts/ChapterContext.tsx";
import { Description } from "@mui/icons-material";
export function ChapterFromId({chapterId, children} : { chapterId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
const chapterContext = useContext(ChapterContext);
const [chapter, setChapter] = useState<IChapter | undefined>(undefined);
chapterContext.GetChapter(chapterId).then(setChapter);
return (
chapter === undefined ?
<Card>
<CardContent>
<Stack direction={"row"} alignItems="center" spacing={2}>
<Card>
</Card>
<Card>
<CardContent>
</CardContent>
<CardActions>
{children}
</CardActions>
</Card>
</Stack>
</CardContent>
</Card>
:
<Chapter chapter={chapter}>{children}</Chapter>
);
}
export function Chapter({chapter, children} : { chapter: IChapter, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
return (
<Card>
<CardContent>
<Stack direction={"row"} alignItems="center" spacing={2}>
<MangaFromId mangaId={chapter.parentMangaId} />
<Card>
<CardContent>
<Link level={"title-lg"} href={chapter.url}>{chapter.title}</Link>
<Typography>Vol. <Chip>{chapter.volumeNumber}</Chip></Typography>
<Typography>Ch. <Chip>{chapter.chapterNumber}</Chip></Typography>
<Tooltip title={chapter.fileName}>
<Description />
</Tooltip>
</CardContent>
<CardActions>
{children}
</CardActions>
</Card>
</Stack>
</CardContent>
</Card>
);
}

View File

@ -1,12 +1,13 @@
import {Badge, Box, Card, CardContent, CardCover, Skeleton, Typography,} from "@mui/joy"; import {Badge, Box, Card, CardContent, CardCover, Skeleton, Typography,} 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 {GetMangaCoverImageUrl} from "../api/Manga.tsx";
import {ApiUriContext, getData} from "../api/fetchApi.tsx"; import {ApiUriContext, getData} from "../api/fetchApi.tsx";
import {MangaReleaseStatus, 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";
import {MangaContext} from "../api/Contexts/MangaContext.tsx";
export const CardWidth = 190; export const CardWidth = 190;
export const CardHeight = 300; export const CardHeight = 300;
@ -23,21 +24,13 @@ const coverCss : CSSProperties = {
} }
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 mangaContext = useContext(MangaContext);
const apiUri = useContext(ApiUriContext); const [manga, setManga] = useState<IManga | undefined>(undefined);
mangaContext.GetManga(mangaId).then(setManga);
const loadManga = useCallback(() => {
GetMangaById(apiUri, mangaId).then(setManga);
},[apiUri, mangaId]);
useEffect(() => {
loadManga();
}, []);
return ( return (
<> manga === undefined ?
{manga === undefined ?
<Badge sx={{margin:"8px !important"}} badgeContent={<Skeleton><img width={"24pt"} height={"24pt"} src={"/blahaj.png"} /></Skeleton>} color={ReleaseStatusToPalette(MangaReleaseStatus.Completed)} size={"lg"}> <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"}}> <Card sx={{height:"fit-content",width:"fit-content"}}>
<CardCover> <CardCover>
@ -51,7 +44,7 @@ export function MangaFromId({mangaId, children} : { mangaId: string, children?:
<Box sx={coverSx}> <Box sx={coverSx}>
<Typography 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"}}>
<Skeleton loading={true} animation={"wave"}> <Skeleton loading={true} animation={"wave"}>
{"x ".repeat(Math.random()*25+5)} {mangaId.split("").splice(0,mangaId.length/2).join(" ")}
</Skeleton> </Skeleton>
</Typography> </Typography>
</Box> </Box>
@ -59,8 +52,7 @@ export function MangaFromId({mangaId, children} : { mangaId: string, children?:
</Card> </Card>
</Badge> </Badge>
: :
<Manga manga={manga} children={children} /> } <Manga manga={manga} children={children} />
</>
); );
} }
@ -91,7 +83,8 @@ export function Manga({manga: manga, children} : { manga: IManga, children?: Rea
const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"]; const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"];
const mangaName = manga.name.length > 30 ? manga.name.substring(0, 27) + "..." : manga.name; const maxLength = 50;
const mangaName = manga.name.length > maxLength ? manga.name.substring(0, maxLength-3) + "..." : manga.name;
return ( return (
<Badge sx={{margin:"8px !important"}} badgeContent={mangaConnector ? <img width={"24pt"} height={"24pt"} src={mangaConnector.iconUrl} /> : manga.mangaConnectorName} color={ReleaseStatusToPalette(manga.releaseStatus)} size={"lg"}> <Badge sx={{margin:"8px !important"}} badgeContent={mangaConnector ? <img width={"24pt"} height={"24pt"} src={mangaConnector.iconUrl} /> : manga.mangaConnectorName} color={ReleaseStatusToPalette(manga.releaseStatus)} size={"lg"}>

View File

@ -0,0 +1,9 @@
import {createContext} from "react";
import IChapter from "../types/IChapter.ts";
export const ChapterContext = createContext<{chapters: IChapter[], GetChapter: (chapterId: string) => Promise<IChapter | undefined>}>(
{
chapters : [],
GetChapter: _ => Promise.resolve(undefined)
}
);

View File

@ -0,0 +1,9 @@
import {createContext} from "react";
import IManga, {DefaultManga} from "../types/IManga.ts";
export const MangaContext = createContext<{mangas: IManga[], GetManga: (mangaId: string) => Promise<IManga | undefined>}>(
{
mangas : [],
GetManga: _ => Promise.resolve(DefaultManga)
}
);