mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-06-03 02:53:01 +02:00
Compare commits
No commits in common. "ddaa066dc71fb6ddfb4206d06a0312fd687bc655" and "4e648bd04e942ac3ccb7fa8a115fcb94e0915586" have entirely different histories.
ddaa066dc7
...
4e648bd04e
@ -10,69 +10,21 @@ 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(() => {
|
||||||
@ -83,24 +35,18 @@ export default function App () {
|
|||||||
return (
|
return (
|
||||||
<ApiUriContext.Provider value={apiUri}>
|
<ApiUriContext.Provider value={apiUri}>
|
||||||
<MangaConnectorContext value={mangaConnectors}>
|
<MangaConnectorContext value={mangaConnectors}>
|
||||||
<MangaContext.Provider value={{mangas, GetManga}}>
|
<Sheet className={"app"}>
|
||||||
<ChapterContext.Provider value={{chapters, GetChapter}}>
|
<Header>
|
||||||
<Sheet className={"app"}>
|
<Badge color={"danger"} invisible={apiConnected} badgeContent={"!"}>
|
||||||
<Header>
|
<Button onClick={() => setShowSettings(true)}>Settings</Button>
|
||||||
<Badge color={"danger"} invisible={apiConnected} badgeContent={"!"}>
|
</Badge>
|
||||||
<Button onClick={() => setShowSettings(true)}>Settings</Button>
|
</Header>
|
||||||
<Button onClick={() => setShowJobs(true)}>Jobs</Button>
|
<Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri} setConnected={setApiConnected} />
|
||||||
</Badge>
|
<Search open={showSearch} setOpen={setShowSearch} />
|
||||||
</Header>
|
<Sheet className={"app-content"}>
|
||||||
<Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri} setConnected={setApiConnected} />
|
<MangaList connected={apiConnected} setShowSearch={setShowSearch} />
|
||||||
<Search open={showSearch} setOpen={setShowSearch} />
|
</Sheet>
|
||||||
<JobsDrawer open={showJobs} connected={apiConnected} setOpen={setShowJobs} />
|
</Sheet>
|
||||||
<Sheet className={"app-content"}>
|
|
||||||
<MangaList connected={apiConnected} setShowSearch={setShowSearch} />
|
|
||||||
</Sheet>
|
|
||||||
</Sheet>
|
|
||||||
</ChapterContext.Provider>
|
|
||||||
</MangaContext.Provider>
|
|
||||||
</MangaConnectorContext>
|
</MangaConnectorContext>
|
||||||
</ApiUriContext.Provider>
|
</ApiUriContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,163 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +1,12 @@
|
|||||||
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 {GetMangaCoverImageUrl} from "../api/Manga.tsx";
|
import {GetMangaById, 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;
|
||||||
@ -24,13 +23,21 @@ 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 mangaContext = useContext(MangaContext);
|
const [manga, setManga] = useState<IManga>();
|
||||||
|
|
||||||
const [manga, setManga] = useState<IManga | undefined>(undefined);
|
const apiUri = useContext(ApiUriContext);
|
||||||
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>
|
||||||
@ -44,7 +51,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"}>
|
||||||
{mangaId.split("").splice(0,mangaId.length/2).join(" ")}
|
{"x ".repeat(Math.random()*25+5)}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@ -52,7 +59,8 @@ export function MangaFromId({mangaId, children} : { mangaId: string, children?:
|
|||||||
</Card>
|
</Card>
|
||||||
</Badge>
|
</Badge>
|
||||||
:
|
:
|
||||||
<Manga manga={manga} children={children} />
|
<Manga manga={manga} children={children} /> }
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,8 +91,7 @@ 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 maxLength = 50;
|
const mangaName = manga.name.length > 30 ? manga.name.substring(0, 27) + "..." : manga.name;
|
||||||
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"}>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import IBackendSettings from "../../api/types/IBackendSettings.ts";
|
import IBackendSettings from "../../api/types/IBackendSettings.ts";
|
||||||
import {useCallback, useContext, useEffect, useState} from "react";
|
import {useCallback, useContext, useState} from "react";
|
||||||
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@ -10,32 +10,26 @@ import {
|
|||||||
Typography
|
Typography
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {GetAprilFoolsToggle, UpdateAprilFoolsToggle} from "../../api/BackendSettings.tsx";
|
import {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);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const UpdateAprilFoolsMode = useCallback((value: boolean) => {
|
||||||
setValue(backendSettings?.aprilFoolsMode??false);
|
UpdateAprilFoolsToggle(apiUri, value)
|
||||||
}, [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));
|
||||||
@ -49,7 +43,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}
|
||||||
checked={value} />
|
defaultChecked={backendSettings?.aprilFoolsMode} />
|
||||||
}>
|
}>
|
||||||
Toggle
|
Toggle
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -1,101 +1,113 @@
|
|||||||
import IBackendSettings from "../../api/types/IBackendSettings.ts";
|
import IBackendSettings from "../../api/types/IBackendSettings.ts";
|
||||||
import {ChangeEvent, useCallback, useContext, useEffect, useRef, useState} from "react";
|
import {useCallback, useContext, useState} from "react";
|
||||||
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
AccordionSummary, ColorPaletteProp, Input, Stack, Switch, Typography,
|
AccordionSummary,
|
||||||
|
ColorPaletteProp,
|
||||||
|
Input,
|
||||||
|
Switch,
|
||||||
|
Typography
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import {
|
import * as React from "react";
|
||||||
GetBWImageToggle,
|
import {UpdateBWImageToggle, UpdateImageCompressionValue} from "../../api/BackendSettings.tsx";
|
||||||
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);
|
||||||
|
|
||||||
useEffect(() => {
|
const [loadingBw, setLoadingBw] = useState<boolean>(false);
|
||||||
setBwImages(backendSettings?.bwImages??false);
|
const [bwInputColor, setBwInputcolor] = useState<ColorPaletteProp>("neutral");
|
||||||
setCompression(backendSettings?.compression??100);
|
|
||||||
}, [backendSettings]);
|
|
||||||
|
|
||||||
const [bwImages, setBwImages] = useState<boolean>(backendSettings?.bwImages??false);
|
const timerRefBw = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
const [bwImagesLoading, setBwImagesLoading] = useState(false);
|
const bwChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const [bwImagesColor, setBwImagesColor] = useState<ColorPaletteProp>("neutral");
|
setBwInputcolor("warning");
|
||||||
const bwTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
clearTimeout(timerRefBw.current);
|
||||||
const bwValueChanged = (e : ChangeEvent<HTMLInputElement>) => {
|
console.log(e);
|
||||||
setBwImages(e.target.checked);
|
timerRefBw.current = setTimeout(() => {
|
||||||
setBwImagesColor("warning");
|
UpdateBw(e.target.checked);
|
||||||
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 [compression, setCompression] = useState<number>(backendSettings?.compression??100);
|
const UpdateBw = useCallback((value: boolean) => {
|
||||||
const [compressionLoading, setCompressionLoading] = useState(false);
|
UpdateBWImageToggle(apiUri, value)
|
||||||
const [compressionColor, setCompressionColor] = useState<ColorPaletteProp>("neutral");
|
.then(() => setBwInputcolor("success"))
|
||||||
const compressionTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
.catch(() => setBwInputcolor("danger"))
|
||||||
const compressionCheckedChanged = (e : ChangeEvent<HTMLInputElement>) => {
|
.finally(() => setLoadingBw(false));
|
||||||
setCompressionColor("warning");
|
}, [apiUri]);
|
||||||
if(!e.target.checked)
|
|
||||||
setCompression(100);
|
const [loadingCompression, setLoadingCompression] = useState<boolean>(false);
|
||||||
else
|
const [compressionInputColor, setCompressionInputColor] = useState<ColorPaletteProp>("neutral");
|
||||||
setCompression(50);
|
const [compressionEnabled, setCompressionEnabled] = useState<boolean>((backendSettings?.compression??100) < 100);
|
||||||
clearTimeout(compressionTimerRef.current);
|
const [compressionValue, setCompressionValue] = useState<number|undefined>(backendSettings?.compression);
|
||||||
bwTimerRef.current = setTimeout(() => {
|
|
||||||
UpdateImageCompression(e.target.checked ? 50 : 100);
|
const timerRefCompression = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
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>) => {
|
|
||||||
setCompressionColor("warning");
|
const compressionEnableChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setCompression(parseInt(e.target.value));
|
setCompressionInputColor("warning");
|
||||||
clearTimeout(compressionTimerRef.current);
|
setCompressionEnabled(e.target.checked);
|
||||||
bwTimerRef.current = setTimeout(() => {
|
clearTimeout(timerRefCompression.current);
|
||||||
UpdateImageCompression(parseInt(e.target.value));
|
timerRefCompression.current = setTimeout(() => {
|
||||||
|
UpdateCompressionLevel(e.target.checked ? compressionValue! : 100);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
const UpdateImageCompression = useCallback((val : number) => {
|
|
||||||
setCompressionLoading(true);
|
const UpdateCompressionLevel = useCallback((value: number)=> {
|
||||||
UpdateImageCompressionValue(apiUri, val)
|
setLoadingCompression(true);
|
||||||
.then(() => GetImageCompressionValue(apiUri))
|
UpdateImageCompressionValue(apiUri, value)
|
||||||
.then(setCompression)
|
.then(() => {
|
||||||
.then(() => setCompressionColor("success"))
|
setCompressionInputColor("success");
|
||||||
.catch(() => setCompressionColor("danger"))
|
setCompressionValue(value);
|
||||||
.finally(() => setCompressionLoading(false));
|
})
|
||||||
},[apiUri]);
|
.catch(() => setCompressionInputColor("danger"))
|
||||||
|
.finally(() => setLoadingCompression(false));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<AccordionSummary>Image Processing</AccordionSummary>
|
<AccordionSummary>Image Processing</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<Stack>
|
<Typography endDecorator={
|
||||||
<Typography endDecorator={
|
<Switch disabled={backendSettings === undefined || loadingBw}
|
||||||
<Switch disabled={backendSettings === undefined || bwImagesLoading}
|
onChange={bwChanged}
|
||||||
onChange={bwValueChanged}
|
color={bwInputColor}
|
||||||
color={bwImagesColor}
|
defaultChecked={backendSettings?.bwImages} />
|
||||||
checked={bwImages} />
|
}>
|
||||||
}>B/W Images</Typography>
|
Black and White Images
|
||||||
<Typography endDecorator={
|
</Typography>
|
||||||
<Input type={"number"} value={compression} onChange={compressionValueChanged} startDecorator={
|
<Typography endDecorator={
|
||||||
<Switch disabled={backendSettings === undefined || compressionLoading}
|
<Switch disabled={backendSettings === undefined || loadingCompression}
|
||||||
onChange={compressionCheckedChanged}
|
onChange={compressionEnableChanged}
|
||||||
color={compressionColor}
|
color={compressionInputColor}
|
||||||
checked={compression < 100} />
|
defaultChecked={compressionEnabled} endDecorator={
|
||||||
} />
|
<Input
|
||||||
}>Compression</Typography>
|
defaultValue={backendSettings?.compression}
|
||||||
</Stack>
|
disabled={!compressionEnabled || loadingCompression}
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
|
@ -7,18 +7,17 @@ import {
|
|||||||
ColorPaletteProp,
|
ColorPaletteProp,
|
||||||
Input
|
Input
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import {KeyboardEventHandler, useCallback, useContext, useEffect, useState} from "react";
|
import {KeyboardEventHandler, useCallback, useContext, useState} from "react";
|
||||||
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
import {GetUserAgent, ResetUserAgent, UpdateUserAgent} from "../../api/BackendSettings.tsx";
|
import {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>(backendSettings?.userAgent??"");
|
const [value, setValue] = useState<string>("");
|
||||||
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)
|
||||||
@ -26,32 +25,26 @@ export default function UserAgent({backendSettings}: {backendSettings?: IBackend
|
|||||||
.catch(() => setColor("danger"))
|
.catch(() => setColor("danger"))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}
|
}
|
||||||
}, [apiUri, value])
|
}, [apiUri])
|
||||||
|
|
||||||
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"}
|
||||||
value={value}
|
defaultValue={backendSettings?.userAgent}
|
||||||
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>
|
||||||
|
@ -84,7 +84,7 @@ export default function Settings({open, setOpen, setApiUri, setConnected}:{open:
|
|||||||
}, [checking]);
|
}, [checking]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer size={"lg"} open={open} onClose={() => setOpen(false)}>
|
<Drawer size={"md"} open={open} onClose={() => setOpen(false)}>
|
||||||
<ModalClose />
|
<ModalClose />
|
||||||
<DialogTitle>Settings</DialogTitle>
|
<DialogTitle>Settings</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,9 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
);
|
|
Loading…
x
Reference in New Issue
Block a user