Compare commits

...

10 Commits

Author SHA1 Message Date
10f48af9fa Styling 2025-04-02 02:17:10 +02:00
5e95c34306 Reset Step to 2 on search drawer close if greater 2 2025-04-02 01:22:50 +02:00
66dcb2f1e6 RequestLimits Reset All position 2025-04-02 01:20:00 +02:00
19e033995d Search Styling 2025-04-02 01:18:49 +02:00
99265bacb2 UserAgent Reset 2025-04-02 01:06:55 +02:00
5385dfd918 Request Limits Reset 2025-04-02 01:04:37 +02:00
d6e4d1d27f Request Limits 2025-04-02 00:52:35 +02:00
288cd77049 April Fools Mode 2025-04-02 00:19:26 +02:00
e605578a34 Chapter Naming Scheme Settings 2025-04-02 00:16:13 +02:00
6340c5ad03 Image Processing Settings 2025-04-02 00:06:57 +02:00
11 changed files with 400 additions and 19 deletions

View File

@ -2,7 +2,7 @@ import Sheet from '@mui/joy/Sheet';
import './App.css'
import Settings from "./Settings.tsx";
import Header from "./Header.tsx";
import {Badge, Button} from "@mui/joy";
import {Badge, Box, Button, Card, CardContent, CardCover, Typography} from "@mui/joy";
import {useState} from "react";
import {ApiUriContext} from "./api/fetchApi.tsx";
import Search from './Components/Search.tsx';
@ -23,12 +23,29 @@ export default function App () {
<Badge color={"danger"} invisible={apiConnected} badgeContent={"!"}>
<Button onClick={() => setShowSettings(true)}>Settings</Button>
</Badge>
<Button onClick={() => setShowSearch(true)}>Search</Button>
</Header>
<Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri} setConnected={setApiConnected} />
<Search open={showSearch} setOpen={setShowSearch} />
<Sheet className={"app-content"}>
<MangaList />
<MangaList connected={apiConnected}>
<Badge invisible>
<Card onClick={() => setShowSearch(true)} sx={{height:"fit-content",width:"fit-content"}}>
<CardCover sx={{margin:"var(--Card-padding)"}}>
<img src={"/blahaj.png"} style={{height:"400px", width:"300px"}} />
</CardCover>
<CardCover sx={{
background: 'rgba(234, 119, 246, 0.14)',
backdropFilter: 'blur(6.9px)',
webkitBackdropFilter: 'blur(6.9px)',
}}/>
<CardContent>
<Box style={{height:"400px", width:"300px"}} >
<Typography level={"h1"}>Search</Typography>
</Box>
</CardContent>
</Card>
</Badge>
</MangaList>
</Sheet>
</Sheet>
</ApiUriContext.Provider>

View File

@ -16,9 +16,9 @@ export default function LinkTag({linkId, color} : { linkId: string | undefined,
}, [linkId]);
return (
<Chip variant={"outlined"} size={"md"} color={color??"primary"}>
<Chip variant={"soft"} size={"sm"} color={color??"primary"}>
<Skeleton variant={"text"} loading={loading}>
<Link href={link?.linkUrl}>{link?.linkProvider??"Load Failed"}</Link>
<Link sx={{textDecoration:"underline"}} level={"body-xs"} href={link?.linkUrl}>{link?.linkProvider??"Load Failed"}</Link>
</Skeleton>
</Chip>
);

View File

@ -108,14 +108,14 @@ export function Manga({manga, children, loading} : { manga: IManga | undefined,
expanded ?
<Box sx={sideSx}>
<Skeleton loading={loading} variant={"text"} level={"title-lg"}>
<Stack direction={"row"} flexWrap={"wrap"} spacing={0.5}>
<Stack direction={"row"} flexWrap={"wrap"} spacing={0.5} sx={{maxHeight:"75px", overflowY:"auto", scrollbarWidth: "thin"}}>
{useManga.authorIds.map(authorId => <AuthorTag key={authorId} authorId={authorId} color={"success"} />)}
{useManga.tags.map(tag => <Chip key={tag} variant={"outlined"} size={"md"} color={"primary"}>{tag}</Chip>)}
{useManga.linkIds.map(linkId => <LinkTag key={linkId} linkId={linkId} color={"danger"} />)}
{useManga.tags.map(tag => <Chip key={tag} variant={"soft"} size={"md"} color={"primary"}>{tag}</Chip>)}
{useManga.linkIds.map(linkId => <LinkTag key={linkId} linkId={linkId} color={"warning"} />)}
</Stack>
</Skeleton>
<Skeleton loading={loading} sx={{maxHeight:"300px"}}>
<MarkdownPreview source={useManga.description} style={{backgroundColor: "transparent", color: "black"}} />
<MarkdownPreview source={useManga.description} style={{backgroundColor: "transparent", color: "black", maxHeight:"310px", overflowY:"auto", marginTop:"10px", scrollbarWidth: "thin"}} />
</Skeleton>
</Box>
: null

View File

@ -6,13 +6,16 @@ import {JobType} from "../api/types/Jobs/IJob.ts";
import IDownloadAvailableChaptersJob from "../api/types/Jobs/IDownloadAvailableChaptersJob.ts";
import {MangaFromId} from "./Manga.tsx";
import { Remove } from "@mui/icons-material";
import * as React from "react";
export default function MangaList(){
export default function MangaList({connected, children}: {connected: boolean, children?: React.ReactNode} ){
const apiUri = useContext(ApiUriContext);
const [jobList, setJobList] = useState<IDownloadAvailableChaptersJob[]>([]);
const getJobList = useCallback(() => {
if(!connected)
return;
GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob).then((jl) => setJobList(jl as IDownloadAvailableChaptersJob[]));
},[apiUri]);
@ -24,8 +27,21 @@ export default function MangaList(){
getJobList();
}, [apiUri]);
const timerRef = React.useRef<ReturnType<typeof setInterval>>(undefined);
useEffect(() => {
if(!connected){
clearTimeout(timerRef.current);
return;
}else{
timerRef.current = setInterval(() => {
getJobList();
}, 2000);
}
}, [connected,]);
return(
<Stack direction="row" spacing={1}>
{children}
{jobList.map((job) => (
<MangaFromId key={job.mangaId} mangaId={job.mangaId}>
<Button color={"danger"} endDecorator={<Remove />} onClick={() => deleteJob(job.jobId)}>Delete</Button>

View File

@ -48,6 +48,7 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
const [resultsLoading, setResultsLoading] = useState<boolean>(false);
const StartSearch = useCallback((mangaConnector : IMangaConnector | undefined, value: string)=>{
setStep(3);
if(mangaConnector === undefined)
return;
setResults([]);
@ -92,6 +93,7 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
// @ts-ignore
return (
<Drawer size={"lg"} anchor={"right"} open={open} onClose={() => {
if(step > 2)
setStep(2);
setResults([]);
setOpen(false);
@ -99,12 +101,13 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
<ModalClose />
<Stepper orientation={"vertical"} sx={{ height: '100%', width: "calc(100% - 80px)", margin:"40px"}}>
<Step indicator={
<StepIndicator variant="solid" color="primary">
<StepIndicator variant={step==1?"solid":"outlined"} color={mangaConnectors.length < 1 ? "danger" : "primary"}>
1
</StepIndicator>}>
<Skeleton loading={mangaConnectorsLoading}>
<Select
disabled={mangaConnectorsLoading || resultsLoading}
color={mangaConnectors.length < 1 ? "danger" : "neutral"}
disabled={mangaConnectorsLoading || resultsLoading || mangaConnectors.length < 1}
placeholder={"Select Connector"}
slotProps={{
listbox: {
@ -119,13 +122,13 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
setStep(2);
setSelectedMangaConnector(mangaConnectors.find((o) => o.name === newValue));
}}
endDecorator={<Chip size={"sm"} color={"primary"}>{mangaConnectors.length}</Chip>}>
endDecorator={<Chip size={"sm"} color={mangaConnectors.length < 1 ? "danger" : "primary"}>{mangaConnectors.length}</Chip>}>
{mangaConnectors?.map((connector: IMangaConnector) => ConnectorOption(connector))}
</Select>
</Skeleton>
</Step>
<Step indicator={
<StepIndicator variant="solid" color="primary">
<StepIndicator variant={step==2?"solid":"outlined"} color="primary">
2
</StepIndicator>}>
<Input disabled={step < 2 || resultsLoading} placeholder={"Name or Url " + (selectedMangaConnector ? selectedMangaConnector.baseUris[0] : "")} onKeyDown={(e) => {
@ -137,7 +140,7 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
}}/>
</Step>
<Step indicator={
<StepIndicator variant="solid" color="primary">
<StepIndicator variant={step==3?"solid":"outlined"} color="primary">
3
</StepIndicator>}>
<Typography endDecorator={<Chip size={"sm"} color={"primary"}>{results.length}</Chip>}>Results</Typography>

View File

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

View File

@ -0,0 +1,74 @@
import IBackendSettings from "../../api/types/IBackendSettings";
import {
Accordion,
AccordionDetails,
AccordionSummary, Chip,
CircularProgress,
ColorPaletteProp,
Divider,
Input,
Stack, Tooltip, Typography
} from "@mui/joy";
import {KeyboardEventHandler, useCallback, useContext, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx";
import {UpdateChapterNamingScheme} from "../../api/BackendSettings.tsx";
export default function ChapterNamingScheme({backendSettings}: {backendSettings?: IBackendSettings}) {
const apiUri = useContext(ApiUriContext);
const [loading, setLoading] = useState<boolean>(false);
const [value, setValue] = useState<string>("");
const [color, setColor] = useState<ColorPaletteProp>("neutral");
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
if(e.key === "Enter") {
setLoading(true);
UpdateChapterNamingScheme(apiUri, value)
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}
}, [apiUri])
return (
<Accordion>
<AccordionSummary>Chapter Naming Scheme</AccordionSummary>
<AccordionDetails>
<Input disabled={backendSettings === undefined || loading}
placeholder={"Chapter Naming Scheme"}
defaultValue={backendSettings?.chapterNamingScheme}
onKeyDown={keyDown}
onChange={e => setValue(e.target.value)}
color={color}
endDecorator={(loading ? <CircularProgress color={"primary"} size={"sm"} /> : null)}
/>
<Typography level={"title-sm"}>Placeholders:</Typography>
<Stack direction="row" spacing={1} divider={<Divider />}>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"Manga Title"} >
<Chip color={"primary"}>%M</Chip>
</Tooltip>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"Volume Number"} >
<Chip color={"primary"}>%V</Chip>
</Tooltip>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"Chapter Number"} >
<Chip color={"primary"}>%C</Chip>
</Tooltip>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"Chapter Title"} >
<Chip color={"primary"}>%T</Chip>
</Tooltip>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"Year"} >
<Chip color={"primary"}>%Y</Chip>
</Tooltip>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"First Author"} >
<Chip color={"primary"}>%A</Chip>
</Tooltip>
</Stack>
</AccordionDetails>
</Accordion>
);
}

View File

@ -0,0 +1,114 @@
import IBackendSettings from "../../api/types/IBackendSettings.ts";
import {useCallback, useContext, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx";
import {
Accordion,
AccordionDetails,
AccordionSummary,
ColorPaletteProp,
Input,
Switch,
Typography
} from "@mui/joy";
import * as React from "react";
import {UpdateBWImageToggle, UpdateImageCompressionValue} from "../../api/BackendSettings.tsx";
export default function ImageProcessing({backendSettings}: {backendSettings?: IBackendSettings}) {
const apiUri = useContext(ApiUriContext);
const [loadingBw, setLoadingBw] = useState<boolean>(false);
const [bwInputColor, setBwInputcolor] = useState<ColorPaletteProp>("neutral");
const timerRefBw = React.useRef<ReturnType<typeof setTimeout>>(undefined);
const bwChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
setBwInputcolor("warning");
clearTimeout(timerRefBw.current);
console.log(e);
timerRefBw.current = setTimeout(() => {
UpdateBw(e.target.checked);
}, 1000);
}
const UpdateBw = useCallback((value: boolean) => {
UpdateBWImageToggle(apiUri, value)
.then(() => setBwInputcolor("success"))
.catch(() => setBwInputcolor("danger"))
.finally(() => setLoadingBw(false));
}, [apiUri]);
const [loadingCompression, setLoadingCompression] = useState<boolean>(false);
const [compressionInputColor, setCompressionInputColor] = useState<ColorPaletteProp>("neutral");
const [compressionEnabled, setCompressionEnabled] = useState<boolean>((backendSettings?.compression??100) < 100);
const [compressionValue, setCompressionValue] = useState<number|undefined>(backendSettings?.compression);
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);
}
const compressionEnableChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
setCompressionInputColor("warning");
setCompressionEnabled(e.target.checked);
clearTimeout(timerRefCompression.current);
timerRefCompression.current = setTimeout(() => {
UpdateCompressionLevel(e.target.checked ? compressionValue! : 100);
}, 1000);
}
const UpdateCompressionLevel = useCallback((value: number)=> {
setLoadingCompression(true);
UpdateImageCompressionValue(apiUri, value)
.then(() => {
setCompressionInputColor("success");
setCompressionValue(value);
})
.catch(() => setCompressionInputColor("danger"))
.finally(() => setLoadingCompression(false));
}, [apiUri]);
return (
<Accordion>
<AccordionSummary>Image Processing</AccordionSummary>
<AccordionDetails>
<Typography endDecorator={
<Switch disabled={backendSettings === undefined || loadingBw}
onChange={bwChanged}
color={bwInputColor}
defaultChecked={backendSettings?.bwImages} />
}>
Black and White Images
</Typography>
<Typography endDecorator={
<Switch disabled={backendSettings === undefined || loadingCompression}
onChange={compressionEnableChanged}
color={compressionInputColor}
defaultChecked={compressionEnabled} endDecorator={
<Input
defaultValue={backendSettings?.compression}
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>
</Accordion>
);
}

View File

@ -0,0 +1,81 @@
import IBackendSettings from "../../api/types/IBackendSettings.ts";
import {useCallback, useContext, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Button,
ColorPaletteProp,
Input,
Stack,
Typography
} from "@mui/joy";
import {RequestLimitType} from "../../api/types/EnumRequestLimitType.ts";
import {ResetRequestLimit, ResetRequestLimits, UpdateRequestLimit} from "../../api/BackendSettings.tsx";
import {Restore} from "@mui/icons-material";
export default function RequestLimits({backendSettings}: {backendSettings?: IBackendSettings}) {
const apiUri = useContext(ApiUriContext);
const [color, setColor] = useState<ColorPaletteProp>("neutral");
const [loading, setLoading] = useState(false);
const Update = useCallback((target: HTMLInputElement, limit: RequestLimitType) => {
setLoading(true);
UpdateRequestLimit(apiUri, limit, Number.parseInt(target.value))
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
},[apiUri])
const Reset = useCallback((limit: RequestLimitType) => {
setLoading(true);
ResetRequestLimit(apiUri, limit)
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}, [apiUri]);
const ResetAll = useCallback(() => {
setLoading(true);
ResetRequestLimits(apiUri)
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}, [apiUri]);
return (
<Accordion>
<AccordionSummary>Request Limits</AccordionSummary>
<AccordionDetails>
<Stack spacing={1} direction="column">
<Button loading={backendSettings === undefined} onClick={ResetAll} size={"sm"} variant={"outlined"} endDecorator={<Restore />} color={"warning"}>Reset all</Button>
<Item type={RequestLimitType.Default} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
<Item type={RequestLimitType.MangaInfo} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
<Item type={RequestLimitType.MangaImage} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
<Item type={RequestLimitType.MangaDexFeed} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
<Item type={RequestLimitType.MangaDexImage} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
</Stack>
</AccordionDetails>
</Accordion>
);
}
function Item({type, color, loading, backendSettings, Reset, Update}:
{type: RequestLimitType, color: ColorPaletteProp, loading: boolean, backendSettings: IBackendSettings | undefined, Reset: (x: RequestLimitType) => void, Update: (a: HTMLInputElement, x: RequestLimitType) => void}) {
return (
<Input slotProps={{input: {min: 0, max: 360}}}
color={color}
startDecorator={<Typography sx={{width:"140px"}}>{type}</Typography>}
endDecorator={<Button onClick={() => Reset(type)}>Reset</Button>}
disabled={loading} type={"number"}
defaultValue={backendSettings?.requestLimits[type]}
placeholder={"Default"}
required
onKeyDown={(e) => {
if(e.key == "Enter")
Update(e.target as HTMLInputElement, type);
}}
/>
);
}

View File

@ -1,8 +1,15 @@
import IBackendSettings from "../../api/types/IBackendSettings";
import {Accordion, AccordionDetails, AccordionSummary, CircularProgress, ColorPaletteProp, Input} from "@mui/joy";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Button,
ColorPaletteProp,
Input
} from "@mui/joy";
import {KeyboardEventHandler, useCallback, useContext, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx";
import {UpdateUserAgent} from "../../api/BackendSettings.tsx";
import {ResetUserAgent, UpdateUserAgent} from "../../api/BackendSettings.tsx";
export default function UserAgent({backendSettings}: {backendSettings?: IBackendSettings}) {
const apiUri = useContext(ApiUriContext);
@ -20,6 +27,14 @@ export default function UserAgent({backendSettings}: {backendSettings?: IBackend
}
}, [apiUri])
const Reset = useCallback(() => {
setLoading(true);
ResetUserAgent(apiUri)
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}, [apiUri]);
return (
<Accordion>
<AccordionSummary>UserAgent</AccordionSummary>
@ -30,7 +45,7 @@ export default function UserAgent({backendSettings}: {backendSettings?: IBackend
onKeyDown={keyDown}
onChange={e => setValue(e.target.value)}
color={color}
endDecorator={(loading ? <CircularProgress color={"primary"} size={"sm"} /> : null)}
endDecorator={<Button onClick={Reset} loading={loading}>Reset</Button>}
/>
</AccordionDetails>
</Accordion>

View File

@ -15,6 +15,10 @@ import {ApiUriContext} from "./api/fetchApi.tsx";
import IBackendSettings from "./api/types/IBackendSettings.ts";
import { GetSettings } from './api/BackendSettings.tsx';
import UserAgent from "./Components/Settings/UserAgent.tsx";
import ImageProcessing from "./Components/Settings/ImageProcessing.tsx";
import ChapterNamingScheme from "./Components/Settings/ChapterNamingScheme.tsx";
import AprilFoolsMode from './Components/Settings/AprilFoolsMode.tsx';
import RequestLimits from "./Components/Settings/RequestLimits.tsx";
const checkConnection = async (apiUri: string): Promise<boolean> =>{
return fetch(`${apiUri}/swagger/v2/swagger.json`,
@ -101,6 +105,10 @@ export default function Settings({open, setOpen, setApiUri, setConnected}:{open:
</AccordionDetails>
</Accordion>
<UserAgent backendSettings={backendSettings} />
<ImageProcessing backendSettings={backendSettings} />
<ChapterNamingScheme backendSettings={backendSettings} />
<AprilFoolsMode backendSettings={backendSettings} />
<RequestLimits backendSettings={backendSettings} />
</AccordionGroup>
</DialogContent>
</Drawer>