Compare commits

...

6 Commits

Author SHA1 Message Date
a273af5ed9 FlareSolverr Settings 2025-06-17 00:55:47 +02:00
be704d922a Add a DO NOT REPORT disclaimer to "Already Requested" message 2025-06-16 22:48:45 +02:00
692bf3561b Fix Request being wrongly resolved (even if it failed) 2025-06-16 22:47:53 +02:00
e0ec64882b URL Search
Fix MangaConnector color
2025-06-16 22:30:33 +02:00
496e6fdde8 Fix MangaPopup missing setOpen 2025-06-16 21:23:07 +02:00
645d3c8793 Fix Jobs Popup:
- Loading without being opened
- Cluttered view: Make it a table
2025-06-16 19:37:10 +02:00
19 changed files with 271 additions and 134 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

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> <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>
); );
} }

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); 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;
} }

View File

@@ -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>
); );

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"}>

View File

@@ -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) =>

View 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>
);
}

View File

@@ -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>

View File

@@ -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`);
} }

View File

@@ -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,
{ {

View File

@@ -15,4 +15,5 @@ export default interface IBackendSettings {
bwImages: boolean; bwImages: boolean;
startNewJobTimeoutMs: number; startNewJobTimeoutMs: number;
chapterNamingScheme: string; chapterNamingScheme: string;
flareSolverrUrl: string;
} }