Fix Jobs Popup:

- Loading without being opened
- Cluttered view: Make it a table
This commit is contained in:
2025-06-16 19:37:10 +02:00
parent e1b590482c
commit 645d3c8793
3 changed files with 153 additions and 118 deletions

View File

@ -1,9 +1,23 @@
import {ReactElement, useContext, useState} from "react"; import React, {ReactElement, useContext, useState} from "react";
import IChapter from "../api/types/IChapter.ts"; import IChapter from "../api/types/IChapter.ts";
import {Card, CardActions, CardContent, Chip, Link, Stack, Tooltip, Typography} from "@mui/joy"; import {Box, Chip, Link, Stack, Typography} from "@mui/joy";
import {MangaFromId} from "./Manga.tsx"; import {MangaFromId} from "./Manga.tsx";
import {ChapterContext} from "../api/Contexts/ChapterContext.tsx"; import {ChapterContext} from "../api/Contexts/ChapterContext.tsx";
import { Description } from "@mui/icons-material"; import Drawer from "@mui/joy/Drawer";
import ModalClose from "@mui/joy/ModalClose";
export function ChapterPopupFromId({chapterId, open, setOpen, children}: { chapterId: string | null, open: boolean, setOpen: React.Dispatch<React.SetStateAction<boolean>>, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }) {
return (
<Drawer open={open} onClose={() => setOpen(false)}>
<ModalClose />
{
chapterId !== null ?
<ChapterFromId chapterId={chapterId}>{children}</ChapterFromId>
: null
}
</Drawer>
)
}
export function ChapterFromId({chapterId, children} : { chapterId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){ export function ChapterFromId({chapterId, children} : { chapterId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
const chapterContext = useContext(ChapterContext); const chapterContext = useContext(ChapterContext);
@ -13,23 +27,7 @@ export function ChapterFromId({chapterId, children} : { chapterId: string, child
return ( return (
chapter === undefined ? chapter === undefined ?
<Card> null
<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> <Chapter chapter={chapter}>{children}</Chapter>
); );
@ -37,25 +35,15 @@ export function ChapterFromId({chapterId, children} : { chapterId: string, child
export function Chapter({chapter, children} : { chapter: IChapter, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){ export function Chapter({chapter, children} : { chapter: IChapter, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
return ( return (
<Card> <Stack direction={"row"}>
<CardContent>
<Stack direction={"row"} alignItems="center" spacing={2}>
<MangaFromId mangaId={chapter.parentMangaId} /> <MangaFromId mangaId={chapter.parentMangaId} />
<Card> <Box>
<CardContent>
<Link level={"title-lg"} href={chapter.url}>{chapter.title}</Link> <Link level={"title-lg"} href={chapter.url}>{chapter.title}</Link>
<Typography>Vol. <Chip>{chapter.volumeNumber}</Chip></Typography> <Typography>Volume <Chip>{chapter.volumeNumber}</Chip></Typography>
<Typography>Ch. <Chip>{chapter.chapterNumber}</Chip></Typography> <Typography>Chapter <Chip>{chapter.chapterNumber}</Chip></Typography>
<Tooltip title={chapter.fileName}> <Typography>Title <Chip>{chapter.title}</Chip></Typography>
<Description /> </Box>
</Tooltip>
</CardContent>
<CardActions>
{children} {children}
</CardActions>
</Card>
</Stack> </Stack>
</CardContent>
</Card>
); );
} }

View File

@ -1,26 +1,24 @@
import { import {
Card, Button,
CardContent,
Chip,
DialogContent, DialogTitle, DialogContent, DialogTitle,
Drawer, Drawer,
Input, Input,
Option, Option,
Select, Select,
Stack, Stack,
Tooltip, Table,
Typography Typography
} from "@mui/joy"; } from "@mui/joy";
import {GetAllJobs} from "../api/Job.tsx"; import {GetJobsInState, GetJobsOfTypeAndWithState, GetJobsWithType} from "../api/Job.tsx";
import * as React from "react"; import * as React from "react";
import {useCallback, useContext, useEffect, useState} from "react"; import {useCallback, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx"; import {ApiUriContext} from "../api/fetchApi.tsx";
import IJob, {JobState, JobType} from "../api/types/Jobs/IJob.ts"; import IJob, {JobState, JobType} from "../api/types/Jobs/IJob.ts";
import IJobWithMangaId from "../api/types/Jobs/IJobWithMangaId.ts";
import {MangaFromId} from "./Manga.tsx";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import {MangaPopupFromId} from "./MangaPopup.tsx";
import IJobWithMangaId from "../api/types/Jobs/IJobWithMangaId.ts";
import {ChapterPopupFromId} from "./Chapter.tsx";
import IJobWithChapterId from "../api/types/Jobs/IJobWithChapterId.tsx"; import IJobWithChapterId from "../api/types/Jobs/IJobWithChapterId.tsx";
import {ChapterFromId} from "./Chapter.tsx";
export default function JobsDrawer({open, connected, setOpen} : {open: boolean, connected: boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>}) { export default function JobsDrawer({open, connected, setOpen} : {open: boolean, connected: boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>}) {
const apiUri = useContext(ApiUriContext); const apiUri = useContext(ApiUriContext);
@ -36,35 +34,27 @@ export default function JobsDrawer({open, connected, setOpen} : {open: boolean,
const updateDisplayJobs = useCallback(() => { const updateDisplayJobs = useCallback(() => {
if(!connected) if(!connected)
return; return;
GetAllJobs(apiUri).then(setAllJobs); if (filterState === null && filterType === null)
}, [apiUri, connected]); setAllJobs([]);
else if (filterState === null && filterType != null)
GetJobsWithType(apiUri, filterType as unknown as JobType).then(setAllJobs);
else if (filterState != null && filterType === null)
GetJobsInState(apiUri, filterState as unknown as JobState).then(setAllJobs);
else if (filterState != null && filterType != null)
GetJobsOfTypeAndWithState(apiUri, filterType as unknown as JobType, filterState as unknown as JobState).then(setAllJobs);
}, [connected, filterType, filterState]);
const timerRef = React.useRef<ReturnType<typeof setInterval>>(undefined); const timerRef = React.useRef<ReturnType<typeof setInterval>>(undefined);
const updateTimer = useCallback(() => { useEffect(() => {
if(!connected){
clearTimeout(timerRef.current); clearTimeout(timerRef.current);
return;
}else{
if(timerRef.current === undefined) {
console.log("Added timer!");
updateDisplayJobs(); updateDisplayJobs();
timerRef.current = setInterval(() => { timerRef.current = setInterval(updateDisplayJobs, 2000);
updateDisplayJobs(); }, [filterState, filterType]);
}, 2000);
}
}
}, [open, connected]);
const FilterJobs = (jobs? : IJob[] | undefined) : IJob[] => { useEffect(() => {
if(jobs === undefined) if (!open || !connected)
return []; clearTimeout(timerRef.current);
let ret = jobs; }, [open, connected]);
if(filterState != undefined)
ret = ret.filter(job => job.state == filterState);
if(filterType != undefined)
ret = ret.filter(job => job.jobType == filterType);
return ret.sort((a, b) => new Date(a.nextExecution).getDate() - new Date(b.nextExecution).getDate());
}
const handleChangeState = ( const handleChangeState = (
_: React.SyntheticEvent | null, _: React.SyntheticEvent | null,
@ -73,6 +63,7 @@ export default function JobsDrawer({open, connected, setOpen} : {open: boolean,
setFilterState(newValue); setFilterState(newValue);
setPage(1); setPage(1);
}; };
const handleChangeType = ( const handleChangeType = (
_: React.SyntheticEvent | null, _: React.SyntheticEvent | null,
newValue: string | null, newValue: string | null,
@ -81,9 +72,19 @@ export default function JobsDrawer({open, connected, setOpen} : {open: boolean,
setPage(1); setPage(1);
}; };
useEffect(() => { const [mangaPopupOpen, setMangaPopupOpen] = React.useState(false);
updateTimer(); const [selectedMangaId, setSelectedMangaId] = useState<string | null>(null);
}, [open, connected]); const OpenMangaPopupDrawer = (mangaId: string) => {
setSelectedMangaId(mangaId);
setMangaPopupOpen(true);
}
const [chapterPopupOpen, setChapterPopupOpen] = React.useState(false);
const [selectedChapterId, setSelectedChapterId] = React.useState<string | null>(null);
const OpenChapterPopupDrawer = (chapterId: string) => {
setSelectedChapterId(chapterId);
setChapterPopupOpen(true);
}
return ( return (
<Drawer size={"lg"} anchor={"left"} open={open} onClose={() => setOpen(false)}> <Drawer size={"lg"} anchor={"left"} open={open} onClose={() => setOpen(false)}>
@ -105,47 +106,41 @@ export default function JobsDrawer({open, connected, setOpen} : {open: boolean,
<Input type={"number"} <Input type={"number"}
value={page} value={page}
onChange={(e) => setPage(parseInt(e.target.value))} onChange={(e) => setPage(parseInt(e.target.value))}
slotProps={{input: { min: 1, max: Math.ceil(FilterJobs(allJobs).length / pageSize)}}} slotProps={{input: { min: 1, max: Math.ceil(allJobs.length / pageSize)}}}
startDecorator={<Typography>Page</Typography>} startDecorator={<Typography>Page</Typography>}
endDecorator={<Typography>/{Math.ceil(FilterJobs(allJobs).length / pageSize)}</Typography>}/> endDecorator={<Typography>/{Math.ceil(allJobs.length / pageSize)}</Typography>}/>
</Stack> </Stack>
<DialogContent> <DialogContent>
<Stack direction={"column"} spacing={1}> <Table borderAxis={"xBetween"} stickyHeader>
{FilterJobs(allJobs).splice(pageSize*(page-1), pageSize).map(job => <FormatJob key={job.jobId} job={job}/>)} <thead>
</Stack> <tr>
<th>Type</th>
<th>State</th>
<th>Last Execution</th>
<th>NextExecution</th>
<th>Extra</th>
</tr>
</thead>
<tbody>
{allJobs.slice((page-1)*pageSize, page*pageSize).map((job) => (
<tr key={job.jobId}>
<td>{job.jobType}</td>
<td>{job.state}</td>
<td>{new Date(job.lastExecution).toLocaleString()}</td>
<td>{new Date(job.nextExecution).toLocaleString()}</td>
<td>{ExtraContent(job, OpenMangaPopupDrawer, OpenChapterPopupDrawer)}</td>
</tr>
))}
</tbody>
</Table>
</DialogContent> </DialogContent>
<MangaPopupFromId mangaId={selectedMangaId} open={mangaPopupOpen} setOpen={setMangaPopupOpen} />
<ChapterPopupFromId chapterId={selectedChapterId} open={chapterPopupOpen} setOpen={setChapterPopupOpen} />
</Drawer> </Drawer>
) )
} }
function FormatJob({job} : {job: IJob}) { function ExtraContent(job: IJob, OpenMangaPopupDrawer: (mangaId: string) => void, OpenChapterPopupDrawer: (IJobWithChapterId: string) => void){
return (
<Card variant={"solid"}>
<CardContent>
<Tooltip title={job.jobId}>
<Typography level={"title-lg"}>{job.jobType}</Typography>
</Tooltip>
</CardContent>
<CardContent>
<Stack direction={"row"} spacing={1}>
<Tooltip title={"Last Execution"}>
<Chip>{new Date(job.lastExecution).toLocaleString()}</Chip>
</Tooltip>
<Tooltip title={"Next Execution"}>
<Chip>{new Date(job.nextExecution).toLocaleString()}</Chip>
</Tooltip>
<Chip>{job.state}</Chip>
</Stack>
</CardContent>
<CardContent>
{ExtraContent(job)}
</CardContent>
</Card>
);
}
function ExtraContent(job: IJob){
switch(job.jobType){ switch(job.jobType){
case JobType.DownloadAvailableChaptersJob: case JobType.DownloadAvailableChaptersJob:
case JobType.DownloadMangaCoverJob: case JobType.DownloadMangaCoverJob:
@ -153,10 +148,10 @@ function ExtraContent(job: IJob){
case JobType.UpdateChaptersDownloadedJob: case JobType.UpdateChaptersDownloadedJob:
case JobType.UpdateCoverJob: case JobType.UpdateCoverJob:
case JobType.MoveMangaLibraryJob: case JobType.MoveMangaLibraryJob:
return <MangaFromId key={(job as IJobWithMangaId).mangaId} mangaId={(job as IJobWithMangaId).mangaId} />; return <Button onClick={() => OpenMangaPopupDrawer((job as IJobWithMangaId).mangaId)}>Open Manga</Button>
case JobType.DownloadSingleChapterJob: case JobType.DownloadSingleChapterJob:
case JobType.UpdateSingleChapterDownloadedJob: case JobType.UpdateSingleChapterDownloadedJob:
return <ChapterFromId key={(job as IJobWithChapterId).chapterId} chapterId={(job as IJobWithChapterId).chapterId} /> return <Button onClick={() => OpenChapterPopupDrawer((job as IJobWithChapterId).chapterId)}>ShowChapter</Button>
default: default:
return null; return null;
} }

View File

@ -1,6 +1,6 @@
import IManga from "../api/types/IManga.ts"; import IManga from "../api/types/IManga.ts";
import {Badge, Box, Chip, CircularProgress, Drawer, Input, Link, 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 React, {ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
import { import {
GetLatestChapterAvailable, GetLatestChapterAvailable,
GetLatestChapterDownloaded, GetLatestChapterDownloaded,
@ -13,9 +13,58 @@ import {CardHeight} from "./Manga.tsx";
import IChapter from "../api/types/IChapter.ts"; import IChapter from "../api/types/IChapter.ts";
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts"; import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx"; import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx";
import {MangaContext} from "../api/Contexts/MangaContext.tsx";
import ModalClose from "@mui/joy/ModalClose";
export default function MangaPopup({manga, open, children} : {manga: IManga | null, open: boolean, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) { export function MangaPopupFromId({mangaId, open, setOpen, children} : {mangaId: string | null, open: boolean, setOpen: React.Dispatch<React.SetStateAction<boolean>>, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
const mangaContext = useContext(MangaContext);
const [manga, setManga] = useState<IManga | undefined>(undefined);
useEffect(() => {
if (!open || mangaId === null)
return;
mangaContext.GetManga(mangaId).then(setManga);
}, [open]);
return (
manga === undefined ?
<Drawer anchor="bottom" size="lg" open={open} onClose={() => setOpen(false)}>
<ModalClose />
<Stack direction="column" spacing={2} margin={"10px"}>
{ /* Cover and Description */ }
<Stack direction="row" spacing={2} margin={"10px"}>
<Badge sx={{margin:"8px !important"}} color={ReleaseStatusToPalette(MangaReleaseStatus.Unreleased)} size={"lg"}>
<img src="/blahaj.png" alt="Manga Cover"/>
</Badge>
<Box>
<Skeleton loading={true} animation={"wave"}>
{mangaId?.split("").splice(0,mangaId.length/2).join(" ")}
</Skeleton>
<Stack direction={"row"} flexWrap={"wrap"} useFlexGap={true} spacing={0.3} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
{mangaId?.split("").filter(x => Number.isNaN(x)).map(_ =>
<Skeleton loading={true} animation={"wave"}>
<Chip>Wow</Chip>
</Skeleton>
)}
</Stack>
<MarkdownPreview 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}>
{children}
</Stack>
</Stack>
</Drawer>
:
<MangaPopup manga={manga} open={open} setOpen={setOpen}>{children}</MangaPopup>
);
}
export default function MangaPopup({manga, open, setOpen, children} : {manga: IManga | null, open: boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
const apiUri = useContext(ApiUriContext); const apiUri = useContext(ApiUriContext);
@ -24,6 +73,8 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
const LoadMangaCover = useCallback(() => { const LoadMangaCover = useCallback(() => {
if(CoverRef.current == null || manga == null) if(CoverRef.current == null || manga == null)
return; return;
if (!open)
return;
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current); const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
if(CoverRef.current.src == coverUrl) if(CoverRef.current.src == coverUrl)
return; return;
@ -32,7 +83,7 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
getData(coverUrl).then(() => { getData(coverUrl).then(() => {
if(CoverRef.current) CoverRef.current.src = coverUrl; if(CoverRef.current) CoverRef.current.src = coverUrl;
}); });
}, [manga, apiUri]) }, [manga, apiUri, open])
useEffect(() => { useEffect(() => {
if(!open) if(!open)
@ -75,7 +126,8 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
const mangaConnector = useContext(MangaConnectorContext).find(all => all.name == manga?.mangaConnectorName); const mangaConnector = useContext(MangaConnectorContext).find(all => all.name == manga?.mangaConnectorName);
return ( return (
<Drawer anchor="bottom" size="lg" open={open}> <Drawer anchor="bottom" size="lg" open={open} onClose={() => setOpen(false)}>
<ModalClose />
<Stack direction="column" spacing={2} margin={"10px"}> <Stack direction="column" spacing={2} margin={"10px"}>
{ /* Cover and Description */ } { /* Cover and Description */ }
<Stack direction="row" spacing={2} margin={"10px"}> <Stack direction="row" spacing={2} margin={"10px"}>