Compare commits

...

6 Commits

Author SHA1 Message Date
ddaa066dc7 Jobs List 2025-05-20 03:14:37 +02:00
a6d2de35cf Jobs List 2025-05-20 03:12:51 +02:00
7cdc31fedb Fix ImageProcessing 2025-05-19 20:52:47 +02:00
dcf596df84 Fix AprilFoolsMode Setting 2025-05-19 20:23:31 +02:00
855fe91e69 Fix UserAgent Setting 2025-05-19 20:23:24 +02:00
c00c80503c Fix UserAgent Setting 2025-05-19 20:19:18 +02:00
10 changed files with 423 additions and 133 deletions

View File

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

View File

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

View File

@ -0,0 +1,163 @@
import {
Card,
CardContent,
Chip,
DialogContent, DialogTitle,
Drawer,
Input,
Option,
Select,
Stack,
Tooltip,
Typography
} from "@mui/joy";
import {GetAllJobs} 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 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);
const [allJobs, setAllJobs] = useState<IJob[]>([]);
const [filterState, setFilterState] = useState<string|null>(null);
const [filterType, setFilterType] = useState<string|null>(null);
const pageSize = 10;
const [page, setPage] = useState(1);
const updateDisplayJobs = useCallback(() => {
if(!connected)
return;
GetAllJobs(apiUri).then(setAllJobs);
}, [apiUri, connected]);
const timerRef = React.useRef<ReturnType<typeof setInterval>>(undefined);
const updateTimer = useCallback(() => {
if(!connected){
clearTimeout(timerRef.current);
return;
}else{
if(timerRef.current === undefined) {
console.log("Added timer!");
updateDisplayJobs();
timerRef.current = setInterval(() => {
updateDisplayJobs();
}, 2000);
}
}
}, [open, connected]);
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());
}
const handleChangeState = (
_: React.SyntheticEvent | null,
newValue: string | null,
) => {
setFilterState(newValue);
setPage(1);
};
const handleChangeType = (
_: React.SyntheticEvent | null,
newValue: string | null,
) => {
setFilterType(newValue);
setPage(1);
};
useEffect(() => {
updateTimer();
}, [open, connected]);
return (
<Drawer size={"lg"} anchor={"left"} open={open} onClose={() => setOpen(false)}>
<ModalClose />
<DialogTitle><Typography level={"h2"}>Jobs</Typography></DialogTitle>
<Stack direction={"row"} spacing={2}>
<Select placeholder={"State"} value={filterState} onChange={handleChangeState} startDecorator={
<Typography>State</Typography>
}>
<Option value={null}>None</Option>
{Object.keys(JobState).map((state) => <Option value={state}>{state}</Option>)}
</Select>
<Select placeholder={"Type"} value={filterType} onChange={handleChangeType} startDecorator={
<Typography>Type</Typography>
}>
<Option value={null}>None</Option>
{Object.keys(JobType).map((type) => <Option value={type}>{type}</Option>)}
</Select>
<Input type={"number"}
value={page}
onChange={(e) => setPage(parseInt(e.target.value))}
slotProps={{input: { min: 1, max: Math.ceil(FilterJobs(allJobs).length / pageSize)}}}
startDecorator={<Typography>Page</Typography>}
endDecorator={<Typography>/{Math.ceil(FilterJobs(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>
</DialogContent>
</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){
switch(job.jobType){
case JobType.DownloadAvailableChaptersJob:
case JobType.DownloadMangaCoverJob:
case JobType.RetrieveChaptersJob:
case JobType.UpdateChaptersDownloadedJob:
case JobType.UpdateCoverJob:
case JobType.MoveMangaLibraryJob:
return <MangaFromId key={(job as IJobWithMangaId).mangaId} mangaId={(job as IJobWithMangaId).mangaId} />;
case JobType.DownloadSingleChapterJob:
case JobType.UpdateSingleChapterDownloadedJob:
return <ChapterFromId key={(job as IJobWithChapterId).chapterId} chapterId={(job as IJobWithChapterId).chapterId} />
default:
return null;
}
}

View File

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

View File

@ -1,5 +1,5 @@
import IBackendSettings from "../../api/types/IBackendSettings.ts"; import IBackendSettings from "../../api/types/IBackendSettings.ts";
import {useCallback, useContext, useState} from "react"; import {useCallback, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx"; import {ApiUriContext} from "../../api/fetchApi.tsx";
import { import {
Accordion, Accordion,
@ -10,26 +10,32 @@ import {
Typography Typography
} from "@mui/joy"; } from "@mui/joy";
import * as React from "react"; import * as React from "react";
import {UpdateAprilFoolsToggle} from "../../api/BackendSettings.tsx"; import {GetAprilFoolsToggle, UpdateAprilFoolsToggle} from "../../api/BackendSettings.tsx";
export default function ImageProcessing({backendSettings}: {backendSettings?: IBackendSettings}) { export default function ImageProcessing({backendSettings}: {backendSettings?: IBackendSettings}) {
const apiUri = useContext(ApiUriContext); const apiUri = useContext(ApiUriContext);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [color, setColor] = useState<ColorPaletteProp>("neutral"); const [color, setColor] = useState<ColorPaletteProp>("neutral");
const [value, setValue] = useState<boolean>(backendSettings?.aprilFoolsMode??false);
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined); const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
const valueChanged = (e : React.ChangeEvent<HTMLInputElement>) => { const valueChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
setColor("warning"); setColor("warning");
clearTimeout(timerRef.current); clearTimeout(timerRef.current);
console.log(e);
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
UpdateAprilFoolsMode(e.target.checked); UpdateAprilFoolsMode(e.target.checked);
}, 1000); }, 1000);
} }
const UpdateAprilFoolsMode = useCallback((value: boolean) => { useEffect(() => {
UpdateAprilFoolsToggle(apiUri, value) setValue(backendSettings?.aprilFoolsMode??false);
}, [backendSettings]);
const UpdateAprilFoolsMode = useCallback((val: boolean) => {
UpdateAprilFoolsToggle(apiUri, val)
.then(() => GetAprilFoolsToggle(apiUri))
.then((val) => setValue(val))
.then(() => setColor("success")) .then(() => setColor("success"))
.catch(() => setColor("danger")) .catch(() => setColor("danger"))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@ -43,7 +49,7 @@ export default function ImageProcessing({backendSettings}: {backendSettings?: IB
<Switch disabled={backendSettings === undefined || loading} <Switch disabled={backendSettings === undefined || loading}
onChange={valueChanged} onChange={valueChanged}
color={color} color={color}
defaultChecked={backendSettings?.aprilFoolsMode} /> checked={value} />
}> }>
Toggle Toggle
</Typography> </Typography>

View File

@ -1,113 +1,101 @@
import IBackendSettings from "../../api/types/IBackendSettings.ts"; import IBackendSettings from "../../api/types/IBackendSettings.ts";
import {useCallback, useContext, useState} from "react"; import {ChangeEvent, useCallback, useContext, useEffect, useRef, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx"; import {ApiUriContext} from "../../api/fetchApi.tsx";
import { import {
Accordion, Accordion,
AccordionDetails, AccordionDetails,
AccordionSummary, AccordionSummary, ColorPaletteProp, Input, Stack, Switch, Typography,
ColorPaletteProp,
Input,
Switch,
Typography
} from "@mui/joy"; } from "@mui/joy";
import * as React from "react"; import {
import {UpdateBWImageToggle, UpdateImageCompressionValue} from "../../api/BackendSettings.tsx"; GetBWImageToggle,
GetImageCompressionValue,
UpdateBWImageToggle,
UpdateImageCompressionValue
} from "../../api/BackendSettings.tsx";
export default function ImageProcessing({backendSettings}: {backendSettings?: IBackendSettings}) { export default function ImageProcessing ({backendSettings}: { backendSettings?: IBackendSettings }) {
const apiUri = useContext(ApiUriContext); const apiUri = useContext(ApiUriContext);
const [loadingBw, setLoadingBw] = useState<boolean>(false); useEffect(() => {
const [bwInputColor, setBwInputcolor] = useState<ColorPaletteProp>("neutral"); setBwImages(backendSettings?.bwImages??false);
setCompression(backendSettings?.compression??100);
}, [backendSettings]);
const timerRefBw = React.useRef<ReturnType<typeof setTimeout>>(undefined); const [bwImages, setBwImages] = useState<boolean>(backendSettings?.bwImages??false);
const bwChanged = (e : React.ChangeEvent<HTMLInputElement>) => { const [bwImagesLoading, setBwImagesLoading] = useState(false);
setBwInputcolor("warning"); const [bwImagesColor, setBwImagesColor] = useState<ColorPaletteProp>("neutral");
clearTimeout(timerRefBw.current); const bwTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
console.log(e); const bwValueChanged = (e : ChangeEvent<HTMLInputElement>) => {
timerRefBw.current = setTimeout(() => { setBwImages(e.target.checked);
UpdateBw(e.target.checked); setBwImagesColor("warning");
clearTimeout(bwTimerRef.current);
bwTimerRef.current = setTimeout(() => {
UpdateBwImages(e.target.checked);
}, 1000); }, 1000);
} }
const UpdateBwImages = useCallback((val : boolean) => {
setBwImagesLoading(true);
UpdateBWImageToggle(apiUri, val)
.then(() => GetBWImageToggle(apiUri))
.then(setBwImages)
.then(() => setBwImagesColor("success"))
.catch(() => setBwImagesColor("danger"))
.finally(() => setBwImagesLoading(false));
},[apiUri]);
const UpdateBw = useCallback((value: boolean) => { const [compression, setCompression] = useState<number>(backendSettings?.compression??100);
UpdateBWImageToggle(apiUri, value) const [compressionLoading, setCompressionLoading] = useState(false);
.then(() => setBwInputcolor("success")) const [compressionColor, setCompressionColor] = useState<ColorPaletteProp>("neutral");
.catch(() => setBwInputcolor("danger")) const compressionTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
.finally(() => setLoadingBw(false)); const compressionCheckedChanged = (e : ChangeEvent<HTMLInputElement>) => {
}, [apiUri]); setCompressionColor("warning");
if(!e.target.checked)
const [loadingCompression, setLoadingCompression] = useState<boolean>(false); setCompression(100);
const [compressionInputColor, setCompressionInputColor] = useState<ColorPaletteProp>("neutral"); else
const [compressionEnabled, setCompressionEnabled] = useState<boolean>((backendSettings?.compression??100) < 100); setCompression(50);
const [compressionValue, setCompressionValue] = useState<number|undefined>(backendSettings?.compression); clearTimeout(compressionTimerRef.current);
bwTimerRef.current = setTimeout(() => {
const timerRefCompression = React.useRef<ReturnType<typeof setTimeout>>(undefined); UpdateImageCompression(e.target.checked ? 50 : 100);
const compressionLevelChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
setCompressionInputColor("warning");
setCompressionValue(Number.parseInt(e.target.value));
clearTimeout(timerRefCompression.current);
console.log(e);
timerRefCompression.current = setTimeout(() => {
UpdateCompressionLevel(Number.parseInt(e.target.value));
}, 1000); }, 1000);
} }
const compressionValueChanged = (e : ChangeEvent<HTMLInputElement>) => {
const compressionEnableChanged = (e : React.ChangeEvent<HTMLInputElement>) => { setCompressionColor("warning");
setCompressionInputColor("warning"); setCompression(parseInt(e.target.value));
setCompressionEnabled(e.target.checked); clearTimeout(compressionTimerRef.current);
clearTimeout(timerRefCompression.current); bwTimerRef.current = setTimeout(() => {
timerRefCompression.current = setTimeout(() => { UpdateImageCompression(parseInt(e.target.value));
UpdateCompressionLevel(e.target.checked ? compressionValue! : 100);
}, 1000); }, 1000);
} }
const UpdateImageCompression = useCallback((val : number) => {
const UpdateCompressionLevel = useCallback((value: number)=> { setCompressionLoading(true);
setLoadingCompression(true); UpdateImageCompressionValue(apiUri, val)
UpdateImageCompressionValue(apiUri, value) .then(() => GetImageCompressionValue(apiUri))
.then(() => { .then(setCompression)
setCompressionInputColor("success"); .then(() => setCompressionColor("success"))
setCompressionValue(value); .catch(() => setCompressionColor("danger"))
}) .finally(() => setCompressionLoading(false));
.catch(() => setCompressionInputColor("danger")) },[apiUri]);
.finally(() => setLoadingCompression(false));
}, [apiUri]);
return ( return (
<Accordion> <Accordion>
<AccordionSummary>Image Processing</AccordionSummary> <AccordionSummary>Image Processing</AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Typography endDecorator={ <Stack>
<Switch disabled={backendSettings === undefined || loadingBw} <Typography endDecorator={
onChange={bwChanged} <Switch disabled={backendSettings === undefined || bwImagesLoading}
color={bwInputColor} onChange={bwValueChanged}
defaultChecked={backendSettings?.bwImages} /> color={bwImagesColor}
}> checked={bwImages} />
Black and White Images }>B/W Images</Typography>
</Typography> <Typography endDecorator={
<Typography endDecorator={ <Input type={"number"} value={compression} onChange={compressionValueChanged} startDecorator={
<Switch disabled={backendSettings === undefined || loadingCompression} <Switch disabled={backendSettings === undefined || compressionLoading}
onChange={compressionEnableChanged} onChange={compressionCheckedChanged}
color={compressionInputColor} color={compressionColor}
defaultChecked={compressionEnabled} endDecorator={ checked={compression < 100} />
<Input } />
defaultValue={backendSettings?.compression} }>Compression</Typography>
disabled={!compressionEnabled || loadingCompression} </Stack>
onChange={compressionLevelChanged}
color={compressionInputColor}
onKeyDown={(e) => {
if(e.key === "Enter") {
clearTimeout(timerRefCompression.current);
// @ts-ignore
UpdateCompressionLevel(Number.parseInt(e.target.value));
}
}}
sx={{width:"70px"}}
/>
} />
}>
Image Compression
</Typography>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
); );

View File

@ -7,17 +7,18 @@ import {
ColorPaletteProp, ColorPaletteProp,
Input Input
} from "@mui/joy"; } from "@mui/joy";
import {KeyboardEventHandler, useCallback, useContext, useState} from "react"; import {KeyboardEventHandler, useCallback, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx"; import {ApiUriContext} from "../../api/fetchApi.tsx";
import {ResetUserAgent, UpdateUserAgent} from "../../api/BackendSettings.tsx"; import {GetUserAgent, ResetUserAgent, UpdateUserAgent} from "../../api/BackendSettings.tsx";
export default function UserAgent({backendSettings}: {backendSettings?: IBackendSettings}) { export default function UserAgent({backendSettings}: {backendSettings?: IBackendSettings}) {
const apiUri = useContext(ApiUriContext); const apiUri = useContext(ApiUriContext);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [value, setValue] = useState<string>(""); const [value, setValue] = useState<string>(backendSettings?.userAgent??"");
const [color, setColor] = useState<ColorPaletteProp>("neutral"); const [color, setColor] = useState<ColorPaletteProp>("neutral");
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => { const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
if(value === undefined) return;
if(e.key === "Enter") { if(e.key === "Enter") {
setLoading(true); setLoading(true);
UpdateUserAgent(apiUri, value) UpdateUserAgent(apiUri, value)
@ -25,26 +26,32 @@ export default function UserAgent({backendSettings}: {backendSettings?: IBackend
.catch(() => setColor("danger")) .catch(() => setColor("danger"))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
} }
}, [apiUri]) }, [apiUri, value])
const Reset = useCallback(() => { const Reset = useCallback(() => {
setLoading(true); setLoading(true);
ResetUserAgent(apiUri) ResetUserAgent(apiUri)
.then(() => GetUserAgent(apiUri))
.then((val) => setValue(val))
.then(() => setColor("success")) .then(() => setColor("success"))
.catch(() => setColor("danger")) .catch(() => setColor("danger"))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [apiUri]); }, [apiUri]);
useEffect(() => {
setValue(backendSettings?.userAgent??"");
}, [backendSettings]);
return ( return (
<Accordion> <Accordion>
<AccordionSummary>UserAgent</AccordionSummary> <AccordionSummary>UserAgent</AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Input disabled={backendSettings === undefined || loading} <Input disabled={backendSettings === undefined || loading}
placeholder={"UserAgent"} placeholder={"UserAgent"}
defaultValue={backendSettings?.userAgent} value={value}
onKeyDown={keyDown} onKeyDown={keyDown}
onChange={e => setValue(e.target.value)} onChange={e => setValue(e.target.value)}
color={color} color={color}
endDecorator={<Button onClick={Reset} loading={loading}>Reset</Button>} endDecorator={<Button onClick={Reset} loading={loading}>Reset</Button>}
/> />
</AccordionDetails> </AccordionDetails>

View File

@ -84,7 +84,7 @@ export default function Settings({open, setOpen, setApiUri, setConnected}:{open:
}, [checking]); }, [checking]);
return ( return (
<Drawer size={"md"} open={open} onClose={() => setOpen(false)}> <Drawer size={"lg"} open={open} onClose={() => setOpen(false)}>
<ModalClose /> <ModalClose />
<DialogTitle>Settings</DialogTitle> <DialogTitle>Settings</DialogTitle>
<DialogContent> <DialogContent>

View File

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

View File

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