Compare commits
6 Commits
e1b590482c
...
a273af5ed9
Author | SHA1 | Date | |
---|---|---|---|
a273af5ed9 | |||
be704d922a | |||
692bf3561b | |||
e0ec64882b | |||
496e6fdde8 | |||
645d3c8793 |
Before Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 149 KiB |
Before Width: | Height: | Size: 150 KiB |
Before Width: | Height: | Size: 376 KiB |
Before Width: | Height: | Size: 406 KiB |
Before Width: | Height: | Size: 508 KiB |
Before Width: | Height: | Size: 539 KiB |
Before Width: | Height: | Size: 249 KiB |
Before Width: | Height: | Size: 192 KiB |
@@ -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>
|
<MangaFromId mangaId={chapter.parentMangaId} />
|
||||||
<Stack direction={"row"} alignItems="center" spacing={2}>
|
<Box>
|
||||||
<MangaFromId mangaId={chapter.parentMangaId} />
|
<Link level={"title-lg"} href={chapter.url}>{chapter.title}</Link>
|
||||||
<Card>
|
<Typography>Volume <Chip>{chapter.volumeNumber}</Chip></Typography>
|
||||||
<CardContent>
|
<Typography>Chapter <Chip>{chapter.chapterNumber}</Chip></Typography>
|
||||||
<Link level={"title-lg"} href={chapter.url}>{chapter.title}</Link>
|
<Typography>Title <Chip>{chapter.title}</Chip></Typography>
|
||||||
<Typography>Vol. <Chip>{chapter.volumeNumber}</Chip></Typography>
|
</Box>
|
||||||
<Typography>Ch. <Chip>{chapter.chapterNumber}</Chip></Typography>
|
{children}
|
||||||
<Tooltip title={chapter.fileName}>
|
</Stack>
|
||||||
<Description />
|
|
||||||
</Tooltip>
|
|
||||||
</CardContent>
|
|
||||||
<CardActions>
|
|
||||||
{children}
|
|
||||||
</CardActions>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@@ -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);
|
updateDisplayJobs();
|
||||||
return;
|
timerRef.current = setInterval(updateDisplayJobs, 2000);
|
||||||
}else{
|
}, [filterState, filterType]);
|
||||||
if(timerRef.current === undefined) {
|
|
||||||
console.log("Added timer!");
|
|
||||||
updateDisplayJobs();
|
|
||||||
timerRef.current = setInterval(() => {
|
|
||||||
updateDisplayJobs();
|
|
||||||
}, 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,
|
||||||
@@ -80,10 +71,20 @@ export default function JobsDrawer({open, connected, setOpen} : {open: boolean,
|
|||||||
setFilterType(newValue);
|
setFilterType(newValue);
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@@ -109,7 +109,7 @@ export function Manga({manga: manga, children} : { manga: IManga, children?: Rea
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<MangaPopup manga={manga} open={expanded}>{children}</MangaPopup>
|
<MangaPopup manga={manga} open={expanded} setOpen={setExpanded}>{children}</MangaPopup>
|
||||||
</Card>
|
</Card>
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
@@ -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"}>
|
||||||
|
@@ -21,7 +21,7 @@ import {useCallback, useContext, useEffect, useState} from "react";
|
|||||||
import {ApiUriContext} from "../api/fetchApi.tsx";
|
import {ApiUriContext} from "../api/fetchApi.tsx";
|
||||||
import {GetAllConnectors} from "../api/MangaConnector.tsx";
|
import {GetAllConnectors} from "../api/MangaConnector.tsx";
|
||||||
import IManga from "../api/types/IManga.ts";
|
import IManga from "../api/types/IManga.ts";
|
||||||
import {SearchNameOnConnector} from "../api/Search.tsx";
|
import {SearchNameOnConnector, SearchUrl} from "../api/Search.tsx";
|
||||||
import {Manga} from "./Manga.tsx";
|
import {Manga} from "./Manga.tsx";
|
||||||
import Add from "@mui/icons-material/Add";
|
import Add from "@mui/icons-material/Add";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -32,7 +32,7 @@ import { LibraryBooks } from "@mui/icons-material";
|
|||||||
|
|
||||||
export default function Search({open, setOpen}:{open:boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>}){
|
export default function Search({open, setOpen}:{open:boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>}){
|
||||||
|
|
||||||
const [step, setStep] = useState<number>(1);
|
const [step, setStep] = useState<number>(2);
|
||||||
|
|
||||||
const apiUri = useContext(ApiUriContext);
|
const apiUri = useContext(ApiUriContext);
|
||||||
const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>();
|
const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>();
|
||||||
@@ -48,14 +48,28 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
|||||||
const [resultsLoading, setResultsLoading] = useState<boolean>(false);
|
const [resultsLoading, setResultsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const StartSearch = useCallback((mangaConnector : IMangaConnector | undefined, value: string)=>{
|
const StartSearch = useCallback((mangaConnector : IMangaConnector | undefined, value: string)=>{
|
||||||
setStep(3);
|
if(mangaConnector === undefined && !IsValidUrl(value))
|
||||||
if(mangaConnector === undefined)
|
|
||||||
return;
|
return;
|
||||||
setResults([]);
|
setResults(undefined);
|
||||||
setResultsLoading(true);
|
setResultsLoading(true);
|
||||||
SearchNameOnConnector(apiUri, mangaConnector.name, value).then(setResults).finally(() => setResultsLoading(false));
|
setStep(3);
|
||||||
|
if (IsValidUrl(value)){
|
||||||
|
SearchUrl(apiUri, value).then((r) => setResults([r])).finally(() => setResultsLoading(false));
|
||||||
|
}else if (mangaConnector != undefined){
|
||||||
|
SearchNameOnConnector(apiUri, mangaConnector.name, value).then(setResults).finally(() => setResultsLoading(false));
|
||||||
|
}
|
||||||
},[apiUri])
|
},[apiUri])
|
||||||
|
|
||||||
|
function IsValidUrl(str : string) : boolean {
|
||||||
|
const pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
|
||||||
|
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
|
||||||
|
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
|
||||||
|
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
|
||||||
|
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
|
||||||
|
'(\\#[-a-z\\d_]*)?$','i'); // fragment locator
|
||||||
|
return !!pattern.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
const [localLibraries, setLocalLibraries] = useState<ILocalLibrary[]>();
|
const [localLibraries, setLocalLibraries] = useState<ILocalLibrary[]>();
|
||||||
const [localLibrariesLoading, setLocalLibrariesLoading] = useState<boolean>(true);
|
const [localLibrariesLoading, setLocalLibrariesLoading] = useState<boolean>(true);
|
||||||
const [selectedLibraryId, setSelectedLibraryId] = useState<string>();
|
const [selectedLibraryId, setSelectedLibraryId] = useState<string>();
|
||||||
@@ -101,12 +115,12 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
|||||||
<ModalClose />
|
<ModalClose />
|
||||||
<Stepper orientation={"vertical"} sx={{ height: '100%', width: "calc(100% - 80px)", margin:"40px"}}>
|
<Stepper orientation={"vertical"} sx={{ height: '100%', width: "calc(100% - 80px)", margin:"40px"}}>
|
||||||
<Step indicator={
|
<Step indicator={
|
||||||
<StepIndicator variant={step==1?"solid":"outlined"} color={mangaConnectors?.length??0 < 1 ? "danger" : "primary"}>
|
<StepIndicator variant={step==1?"solid":"outlined"} color={(mangaConnectors?.length??0) < 1 ? "danger" : "primary"}>
|
||||||
1
|
1
|
||||||
</StepIndicator>}>
|
</StepIndicator>}>
|
||||||
<Skeleton loading={mangaConnectorsLoading}>
|
<Skeleton loading={mangaConnectorsLoading}>
|
||||||
<Select
|
<Select
|
||||||
color={mangaConnectors?.length??0 < 1 ? "danger" : "neutral"}
|
color={(mangaConnectors?.length??0) < 1 ? "danger" : "neutral"}
|
||||||
disabled={mangaConnectorsLoading || resultsLoading || mangaConnectors?.length == null || mangaConnectors.length < 1}
|
disabled={mangaConnectorsLoading || resultsLoading || mangaConnectors?.length == null || mangaConnectors.length < 1}
|
||||||
placeholder={"Select Connector"}
|
placeholder={"Select Connector"}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@@ -119,8 +133,9 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
|||||||
sx={{ '--ListItemDecorator-size': '44px', minWidth: 240 }}
|
sx={{ '--ListItemDecorator-size': '44px', minWidth: 240 }}
|
||||||
renderValue={renderValue}
|
renderValue={renderValue}
|
||||||
onChange={(_e, newValue) => {
|
onChange={(_e, newValue) => {
|
||||||
setStep(2);
|
|
||||||
setSelectedMangaConnector(mangaConnectors?.find((o) => o.name === newValue));
|
setSelectedMangaConnector(mangaConnectors?.find((o) => o.name === newValue));
|
||||||
|
setStep(2);
|
||||||
|
setResults(undefined);
|
||||||
}}
|
}}
|
||||||
endDecorator={<Chip size={"sm"} color={mangaConnectors?.length??0 < 1 ? "danger" : "primary"}>{mangaConnectors?.length}</Chip>}>
|
endDecorator={<Chip size={"sm"} color={mangaConnectors?.length??0 < 1 ? "danger" : "primary"}>{mangaConnectors?.length}</Chip>}>
|
||||||
{mangaConnectors?.map((connector: IMangaConnector) => ConnectorOption(connector))}
|
{mangaConnectors?.map((connector: IMangaConnector) => ConnectorOption(connector))}
|
||||||
@@ -131,9 +146,9 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
|||||||
<StepIndicator variant={step==2?"solid":"outlined"} color="primary">
|
<StepIndicator variant={step==2?"solid":"outlined"} color="primary">
|
||||||
2
|
2
|
||||||
</StepIndicator>}>
|
</StepIndicator>}>
|
||||||
<Input disabled={step < 2 || resultsLoading} placeholder={"Name or Url " + (selectedMangaConnector ? selectedMangaConnector.baseUris[0] : "")} onKeyDown={(e) => {
|
<Input disabled={resultsLoading} placeholder={"Name or Url " + (selectedMangaConnector ? selectedMangaConnector.baseUris[0] : "")} onKeyDown={(e) => {
|
||||||
setStep(2);
|
setStep(2);
|
||||||
setResults([]);
|
setResults(undefined);
|
||||||
if(e.key === "Enter") {
|
if(e.key === "Enter") {
|
||||||
StartSearch(selectedMangaConnector, e.currentTarget.value);
|
StartSearch(selectedMangaConnector, e.currentTarget.value);
|
||||||
}
|
}
|
||||||
@@ -143,7 +158,7 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
|||||||
<StepIndicator variant={step==3?"solid":"outlined"} color="primary">
|
<StepIndicator variant={step==3?"solid":"outlined"} color="primary">
|
||||||
3
|
3
|
||||||
</StepIndicator>}>
|
</StepIndicator>}>
|
||||||
<Typography endDecorator={<Chip size={"sm"} color={"primary"}>{results?.length}</Chip>}>Results</Typography>
|
<Typography endDecorator={<Chip size={"sm"} color={"primary"}>{results?.length??"-"}</Chip>}>Results</Typography>
|
||||||
<Skeleton loading={resultsLoading}>
|
<Skeleton loading={resultsLoading}>
|
||||||
<Stack direction={"row"} spacing={1} flexWrap={"wrap"}>
|
<Stack direction={"row"} spacing={1} flexWrap={"wrap"}>
|
||||||
{results?.map((result) =>
|
{results?.map((result) =>
|
||||||
|
72
tranga-website/src/Components/Settings/FlareSolverr.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import IBackendSettings from "../../api/types/IBackendSettings";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Button,
|
||||||
|
ColorPaletteProp,
|
||||||
|
Input, Stack
|
||||||
|
} from "@mui/joy";
|
||||||
|
import {KeyboardEventHandler, useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
|
import {
|
||||||
|
ResetFlareSolverrUrl,
|
||||||
|
SetFlareSolverrUrl, TestFlareSolverrUrl,
|
||||||
|
} from "../../api/BackendSettings.tsx";
|
||||||
|
|
||||||
|
export default function FlareSolverr({backendSettings}: {backendSettings?: IBackendSettings}) {
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [value, setValue] = useState<string>(backendSettings?.flareSolverrUrl??"");
|
||||||
|
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
|
||||||
|
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
|
||||||
|
if(value === undefined) return;
|
||||||
|
if(e.key === "Enter") {
|
||||||
|
setLoading(true);
|
||||||
|
SetFlareSolverrUrl(apiUri, value)
|
||||||
|
.then(() => setColor("success"))
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}
|
||||||
|
}, [apiUri, value])
|
||||||
|
|
||||||
|
const Reset = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
|
ResetFlareSolverrUrl(apiUri)
|
||||||
|
.then(() => Test())
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
const Test = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
|
TestFlareSolverrUrl(apiUri)
|
||||||
|
.then(() => setColor("success"))
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(backendSettings?.flareSolverrUrl??"");
|
||||||
|
}, [backendSettings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary>FlareSolverr</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Input disabled={backendSettings === undefined || loading}
|
||||||
|
placeholder={"FlareSolverr URL"}
|
||||||
|
value={value}
|
||||||
|
onKeyDown={keyDown}
|
||||||
|
onChange={e => setValue(e.target.value)}
|
||||||
|
color={color}
|
||||||
|
endDecorator={<Stack direction={"row"} spacing={1}>
|
||||||
|
<Button onClick={Reset} loading={loading}>Reset</Button>
|
||||||
|
<Button onClick={Test} loading={loading}>Test</Button>
|
||||||
|
</Stack>}
|
||||||
|
/>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
@@ -19,6 +19,7 @@ import ImageProcessing from "./Components/Settings/ImageProcessing.tsx";
|
|||||||
import ChapterNamingScheme from "./Components/Settings/ChapterNamingScheme.tsx";
|
import ChapterNamingScheme from "./Components/Settings/ChapterNamingScheme.tsx";
|
||||||
import AprilFoolsMode from './Components/Settings/AprilFoolsMode.tsx';
|
import AprilFoolsMode from './Components/Settings/AprilFoolsMode.tsx';
|
||||||
import RequestLimits from "./Components/Settings/RequestLimits.tsx";
|
import RequestLimits from "./Components/Settings/RequestLimits.tsx";
|
||||||
|
import FlareSolverr from "./Components/Settings/FlareSolverr.tsx";
|
||||||
|
|
||||||
const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
||||||
return fetch(`${apiUri}/swagger/v2/swagger.json`,
|
return fetch(`${apiUri}/swagger/v2/swagger.json`,
|
||||||
@@ -113,6 +114,7 @@ export default function Settings({open, setOpen, setApiUri, setConnected}:{open:
|
|||||||
<ChapterNamingScheme backendSettings={backendSettings} />
|
<ChapterNamingScheme backendSettings={backendSettings} />
|
||||||
<AprilFoolsMode backendSettings={backendSettings} />
|
<AprilFoolsMode backendSettings={backendSettings} />
|
||||||
<RequestLimits backendSettings={backendSettings} />
|
<RequestLimits backendSettings={backendSettings} />
|
||||||
|
<FlareSolverr backendSettings={backendSettings} />
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import {deleteData, getData, patchData} from './fetchApi.tsx';
|
import {deleteData, getData, patchData, postData} from './fetchApi.tsx';
|
||||||
import IBackendSettings from "./types/IBackendSettings.ts";
|
import IBackendSettings from "./types/IBackendSettings.ts";
|
||||||
import IRequestLimits from "./types/IRequestLimits.ts";
|
import IRequestLimits from "./types/IRequestLimits.ts";
|
||||||
import {RequestLimitType} from "./types/EnumRequestLimitType.ts";
|
import {RequestLimitType} from "./types/EnumRequestLimitType.ts";
|
||||||
@@ -77,4 +77,16 @@ export const GetChapterNamingScheme = async (apiUri: string) : Promise<string> =
|
|||||||
|
|
||||||
export const UpdateChapterNamingScheme = async (apiUri: string, value: string) => {
|
export const UpdateChapterNamingScheme = async (apiUri: string, value: string) => {
|
||||||
return patchData(`${apiUri}/v2/Settings/ChapterNamingScheme`, value);
|
return patchData(`${apiUri}/v2/Settings/ChapterNamingScheme`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SetFlareSolverrUrl = async (apiUri: string, value: string) => {
|
||||||
|
return postData(`${apiUri}/v2/Settings/FlareSolverr/Url`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResetFlareSolverrUrl = async (apiUri: string) => {
|
||||||
|
return deleteData(`${apiUri}/v2/Settings/FlareSolverr/Url`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TestFlareSolverrUrl = async (apiUri: string) => {
|
||||||
|
return postData(`${apiUri}/v2/Settings/FlareSolverr/Test`);
|
||||||
}
|
}
|
@@ -27,7 +27,7 @@ function makeRequestWrapper(method: string, uri: string, content?: object | stri
|
|||||||
.then((result) => result as Promise<object>)
|
.then((result) => result as Promise<object>)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
return Promise.resolve(undefined);
|
return Promise.reject(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ 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(`DO NOT REPORT! Already requested: ${method} ${uri}`);
|
||||||
currentlyRequestedEndpoints.push(id);
|
currentlyRequestedEndpoints.push(id);
|
||||||
return fetch(uri,
|
return fetch(uri,
|
||||||
{
|
{
|
||||||
|
@@ -15,4 +15,5 @@ export default interface IBackendSettings {
|
|||||||
bwImages: boolean;
|
bwImages: boolean;
|
||||||
startNewJobTimeoutMs: number;
|
startNewJobTimeoutMs: number;
|
||||||
chapterNamingScheme: string;
|
chapterNamingScheme: string;
|
||||||
|
flareSolverrUrl: string;
|
||||||
}
|
}
|
||||||
|