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 {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 {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 }){
|
||||
const chapterContext = useContext(ChapterContext);
|
||||
@@ -13,23 +27,7 @@ export function ChapterFromId({chapterId, children} : { chapterId: string, child
|
||||
|
||||
return (
|
||||
chapter === undefined ?
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Stack direction={"row"} alignItems="center" spacing={2}>
|
||||
<Card>
|
||||
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent>
|
||||
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
{children}
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
null
|
||||
:
|
||||
<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 }){
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Stack direction={"row"} alignItems="center" spacing={2}>
|
||||
<Stack direction={"row"}>
|
||||
<MangaFromId mangaId={chapter.parentMangaId} />
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box>
|
||||
<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>
|
||||
<Typography>Volume <Chip>{chapter.volumeNumber}</Chip></Typography>
|
||||
<Typography>Chapter <Chip>{chapter.chapterNumber}</Chip></Typography>
|
||||
<Typography>Title <Chip>{chapter.title}</Chip></Typography>
|
||||
</Box>
|
||||
{children}
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -1,26 +1,24 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
Button,
|
||||
DialogContent, DialogTitle,
|
||||
Drawer,
|
||||
Input,
|
||||
Option,
|
||||
Select,
|
||||
Stack,
|
||||
Tooltip,
|
||||
Table,
|
||||
Typography
|
||||
} from "@mui/joy";
|
||||
import {GetAllJobs} from "../api/Job.tsx";
|
||||
import {GetJobsInState, GetJobsOfTypeAndWithState, GetJobsWithType} from "../api/Job.tsx";
|
||||
import * as React from "react";
|
||||
import {useCallback, useContext, useEffect, useState} from "react";
|
||||
import {ApiUriContext} from "../api/fetchApi.tsx";
|
||||
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 {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 {ChapterFromId} from "./Chapter.tsx";
|
||||
|
||||
export default function JobsDrawer({open, connected, setOpen} : {open: boolean, connected: boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>}) {
|
||||
const apiUri = useContext(ApiUriContext);
|
||||
@@ -36,35 +34,27 @@ export default function JobsDrawer({open, connected, setOpen} : {open: boolean,
|
||||
const updateDisplayJobs = useCallback(() => {
|
||||
if(!connected)
|
||||
return;
|
||||
GetAllJobs(apiUri).then(setAllJobs);
|
||||
}, [apiUri, connected]);
|
||||
if (filterState === null && filterType === null)
|
||||
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 updateTimer = useCallback(() => {
|
||||
if(!connected){
|
||||
useEffect(() => {
|
||||
clearTimeout(timerRef.current);
|
||||
return;
|
||||
}else{
|
||||
if(timerRef.current === undefined) {
|
||||
console.log("Added timer!");
|
||||
updateDisplayJobs();
|
||||
timerRef.current = setInterval(() => {
|
||||
updateDisplayJobs();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
}, [open, connected]);
|
||||
timerRef.current = setInterval(updateDisplayJobs, 2000);
|
||||
}, [filterState, filterType]);
|
||||
|
||||
const FilterJobs = (jobs? : IJob[] | undefined) : IJob[] => {
|
||||
if(jobs === undefined)
|
||||
return [];
|
||||
let ret = jobs;
|
||||
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());
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!open || !connected)
|
||||
clearTimeout(timerRef.current);
|
||||
}, [open, connected]);
|
||||
|
||||
const handleChangeState = (
|
||||
_: React.SyntheticEvent | null,
|
||||
@@ -73,6 +63,7 @@ export default function JobsDrawer({open, connected, setOpen} : {open: boolean,
|
||||
setFilterState(newValue);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleChangeType = (
|
||||
_: React.SyntheticEvent | null,
|
||||
newValue: string | null,
|
||||
@@ -81,9 +72,19 @@ export default function JobsDrawer({open, connected, setOpen} : {open: boolean,
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateTimer();
|
||||
}, [open, connected]);
|
||||
const [mangaPopupOpen, setMangaPopupOpen] = React.useState(false);
|
||||
const [selectedMangaId, setSelectedMangaId] = useState<string | null>(null);
|
||||
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 (
|
||||
<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"}
|
||||
value={page}
|
||||
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>}
|
||||
endDecorator={<Typography>/{Math.ceil(FilterJobs(allJobs).length / pageSize)}</Typography>}/>
|
||||
endDecorator={<Typography>/{Math.ceil(allJobs.length / pageSize)}</Typography>}/>
|
||||
</Stack>
|
||||
<DialogContent>
|
||||
<Stack direction={"column"} spacing={1}>
|
||||
{FilterJobs(allJobs).splice(pageSize*(page-1), pageSize).map(job => <FormatJob key={job.jobId} job={job}/>)}
|
||||
</Stack>
|
||||
<Table borderAxis={"xBetween"} stickyHeader>
|
||||
<thead>
|
||||
<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>
|
||||
<MangaPopupFromId mangaId={selectedMangaId} open={mangaPopupOpen} setOpen={setMangaPopupOpen} />
|
||||
<ChapterPopupFromId chapterId={selectedChapterId} open={chapterPopupOpen} setOpen={setChapterPopupOpen} />
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
function FormatJob({job} : {job: IJob}) {
|
||||
|
||||
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){
|
||||
function ExtraContent(job: IJob, OpenMangaPopupDrawer: (mangaId: string) => void, OpenChapterPopupDrawer: (IJobWithChapterId: string) => void){
|
||||
switch(job.jobType){
|
||||
case JobType.DownloadAvailableChaptersJob:
|
||||
case JobType.DownloadMangaCoverJob:
|
||||
@@ -153,10 +148,10 @@ function ExtraContent(job: IJob){
|
||||
case JobType.UpdateChaptersDownloadedJob:
|
||||
case JobType.UpdateCoverJob:
|
||||
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.UpdateSingleChapterDownloadedJob:
|
||||
return <ChapterFromId key={(job as IJobWithChapterId).chapterId} chapterId={(job as IJobWithChapterId).chapterId} />
|
||||
return <Button onClick={() => OpenChapterPopupDrawer((job as IJobWithChapterId).chapterId)}>ShowChapter</Button>
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@@ -109,7 +109,7 @@ export function Manga({manga: manga, children} : { manga: IManga, children?: Rea
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
<MangaPopup manga={manga} open={expanded}>{children}</MangaPopup>
|
||||
<MangaPopup manga={manga} open={expanded} setOpen={setExpanded}>{children}</MangaPopup>
|
||||
</Card>
|
||||
</Badge>
|
||||
);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import IManga from "../api/types/IManga.ts";
|
||||
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 {
|
||||
GetLatestChapterAvailable,
|
||||
GetLatestChapterDownloaded,
|
||||
@@ -13,9 +13,58 @@ import {CardHeight} from "./Manga.tsx";
|
||||
import IChapter from "../api/types/IChapter.ts";
|
||||
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
|
||||
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);
|
||||
|
||||
@@ -24,6 +73,8 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
|
||||
const LoadMangaCover = useCallback(() => {
|
||||
if(CoverRef.current == null || manga == null)
|
||||
return;
|
||||
if (!open)
|
||||
return;
|
||||
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
|
||||
if(CoverRef.current.src == coverUrl)
|
||||
return;
|
||||
@@ -32,7 +83,7 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
|
||||
getData(coverUrl).then(() => {
|
||||
if(CoverRef.current) CoverRef.current.src = coverUrl;
|
||||
});
|
||||
}, [manga, apiUri])
|
||||
}, [manga, apiUri, open])
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
|
||||
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"}>
|
||||
{ /* Cover and Description */ }
|
||||
<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 {GetAllConnectors} from "../api/MangaConnector.tsx";
|
||||
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 Add from "@mui/icons-material/Add";
|
||||
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>>}){
|
||||
|
||||
const [step, setStep] = useState<number>(1);
|
||||
const [step, setStep] = useState<number>(2);
|
||||
|
||||
const apiUri = useContext(ApiUriContext);
|
||||
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 StartSearch = useCallback((mangaConnector : IMangaConnector | undefined, value: string)=>{
|
||||
setStep(3);
|
||||
if(mangaConnector === undefined)
|
||||
if(mangaConnector === undefined && !IsValidUrl(value))
|
||||
return;
|
||||
setResults([]);
|
||||
setResults(undefined);
|
||||
setResultsLoading(true);
|
||||
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])
|
||||
|
||||
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 [localLibrariesLoading, setLocalLibrariesLoading] = useState<boolean>(true);
|
||||
const [selectedLibraryId, setSelectedLibraryId] = useState<string>();
|
||||
@@ -101,12 +115,12 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
||||
<ModalClose />
|
||||
<Stepper orientation={"vertical"} sx={{ height: '100%', width: "calc(100% - 80px)", margin:"40px"}}>
|
||||
<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
|
||||
</StepIndicator>}>
|
||||
<Skeleton loading={mangaConnectorsLoading}>
|
||||
<Select
|
||||
color={mangaConnectors?.length??0 < 1 ? "danger" : "neutral"}
|
||||
color={(mangaConnectors?.length??0) < 1 ? "danger" : "neutral"}
|
||||
disabled={mangaConnectorsLoading || resultsLoading || mangaConnectors?.length == null || mangaConnectors.length < 1}
|
||||
placeholder={"Select Connector"}
|
||||
slotProps={{
|
||||
@@ -119,8 +133,9 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
||||
sx={{ '--ListItemDecorator-size': '44px', minWidth: 240 }}
|
||||
renderValue={renderValue}
|
||||
onChange={(_e, newValue) => {
|
||||
setStep(2);
|
||||
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>}>
|
||||
{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">
|
||||
2
|
||||
</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);
|
||||
setResults([]);
|
||||
setResults(undefined);
|
||||
if(e.key === "Enter") {
|
||||
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">
|
||||
3
|
||||
</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}>
|
||||
<Stack direction={"row"} spacing={1} flexWrap={"wrap"}>
|
||||
{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 AprilFoolsMode from './Components/Settings/AprilFoolsMode.tsx';
|
||||
import RequestLimits from "./Components/Settings/RequestLimits.tsx";
|
||||
import FlareSolverr from "./Components/Settings/FlareSolverr.tsx";
|
||||
|
||||
const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
||||
return fetch(`${apiUri}/swagger/v2/swagger.json`,
|
||||
@@ -113,6 +114,7 @@ export default function Settings({open, setOpen, setApiUri, setConnected}:{open:
|
||||
<ChapterNamingScheme backendSettings={backendSettings} />
|
||||
<AprilFoolsMode backendSettings={backendSettings} />
|
||||
<RequestLimits backendSettings={backendSettings} />
|
||||
<FlareSolverr backendSettings={backendSettings} />
|
||||
</AccordionGroup>
|
||||
</DialogContent>
|
||||
</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 IRequestLimits from "./types/IRequestLimits.ts";
|
||||
import {RequestLimitType} from "./types/EnumRequestLimitType.ts";
|
||||
@@ -78,3 +78,15 @@ export const GetChapterNamingScheme = async (apiUri: string) : Promise<string> =
|
||||
export const UpdateChapterNamingScheme = async (apiUri: string, value: string) => {
|
||||
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>)
|
||||
.catch((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> {
|
||||
const id = method + uri;
|
||||
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);
|
||||
return fetch(uri,
|
||||
{
|
||||
|
@@ -15,4 +15,5 @@ export default interface IBackendSettings {
|
||||
bwImages: boolean;
|
||||
startNewJobTimeoutMs: number;
|
||||
chapterNamingScheme: string;
|
||||
flareSolverrUrl: string;
|
||||
}
|
||||
|