Maintenance Settings

This commit is contained in:
2025-07-22 21:55:46 +02:00
parent 5f74743d8c
commit 34dcd06ca9
10 changed files with 221 additions and 50 deletions

View File

@@ -0,0 +1,32 @@
import {Close, Done} from "@mui/icons-material";
import {CircularProgress, ColorPaletteProp} from "@mui/joy";
import {ReactNode} from "react";
export enum LoadingState {
none,
loading,
success,
failure
}
export function StateIndicator(state : LoadingState) : ReactNode {
switch (state) {
case LoadingState.loading:
return (<CircularProgress />);
case LoadingState.failure:
return (<Close />);
case LoadingState.success:
return (<Done />);
default: return null;
}
}
export function StateColor(state : LoadingState) : ColorPaletteProp | undefined {
switch (state) {
case LoadingState.failure:
return "danger";
case LoadingState.success:
return "success";
default: return undefined;
}
}

View File

@@ -19,7 +19,6 @@ import ModalClose from "@mui/joy/ModalClose";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import {ApiContext} from "../../apiClient/ApiContext.tsx";
import MarkdownPreview from '@uiw/react-markdown-preview'; import MarkdownPreview from '@uiw/react-markdown-preview';
import {MangaContext} from "../../App.tsx"; import {MangaContext} from "../../App.tsx";
import MangaDownloadDialog from "./MangaDownloadDialog.tsx";
import {MangaConnectorLinkFromId} from "../MangaConnectorLink.tsx"; import {MangaConnectorLinkFromId} from "../MangaConnectorLink.tsx";
export function MangaCardFromId({mangaId} : {mangaId: string}) { export function MangaCardFromId({mangaId} : {mangaId: string}) {
@@ -29,7 +28,7 @@ export function MangaCardFromId({mangaId} : {mangaId: string}) {
return <MangaCard manga={manga} /> return <MangaCard manga={manga} />
} }
export function MangaCard({manga} : {manga: Manga | undefined}) { export function MangaCard({manga, children} : {manga: Manga | undefined, children? : ReactNode}) {
if (manga === undefined) if (manga === undefined)
return PlaceHolderCard(); return PlaceHolderCard();
@@ -47,7 +46,7 @@ export function MangaCard({manga} : {manga: Manga | undefined}) {
</CardContent> </CardContent>
</Card> </Card>
<MangaModal manga={manga} open={open} setOpen={setOpen}> <MangaModal manga={manga} open={open} setOpen={setOpen}>
<MangaDownloadDialog manga={manga} /> {children}
</MangaModal> </MangaModal>
</MangaConnectorBadge> </MangaConnectorBadge>
); );
@@ -72,12 +71,12 @@ export function MangaModal({manga, open, setOpen, children}: {manga: Manga | und
{manga?.mangaTags?.map((tag) => <Chip key={tag.tag}>{tag.tag}</Chip>)} {manga?.mangaTags?.map((tag) => <Chip key={tag.tag}>{tag.tag}</Chip>)}
{manga?.links?.map((link) => <Chip key={link.key}><Link href={link.linkUrl}>{link.linkProvider}</Link></Chip>)} {manga?.links?.map((link) => <Chip key={link.key}><Link href={link.linkUrl}>{link.linkProvider}</Link></Chip>)}
</Stack> </Stack>
<Box> <Box sx={{flexGrow: 1}}>
<MarkdownPreview source={manga?.description} style={{background: "transparent"}}/> <MarkdownPreview source={manga?.description} style={{background: "transparent"}}/>
</Box> </Box>
<Stack sx={{justifySelf: "flex-end", alignSelf: "flex-end"}} spacing={2} direction={"row"}>{children}</Stack>
</Stack> </Stack>
</Stack> </Stack>
{children}
</ModalDialog> </ModalDialog>
</Modal> </Modal>
); );

View File

@@ -3,13 +3,25 @@ import {MangaCard} from "./MangaCard.tsx";
import {Stack} from "@mui/joy"; import {Stack} from "@mui/joy";
import "./MangaList.css"; import "./MangaList.css";
import {Manga} from "../../apiClient/data-contracts.ts"; import {Manga} from "../../apiClient/data-contracts.ts";
import MangaDownloadDialog from "./MangaDownloadDialog.tsx";
import MangaMerge from "./MangaMerge.tsx";
export default function MangaList ({mangas, children} : {mangas: Manga[], children?: ReactNode}) { export default function MangaList ({mangas, children} : {mangas: Manga[], children?: ReactNode}) {
return ( return (
<Stack className={"manga-list"} direction={"row"} useFlexGap={true} spacing={2} flexWrap={"wrap"}> <Stack className={"manga-list"} direction={"row"} useFlexGap={true} spacing={2} flexWrap={"wrap"} sx={
{
mx: 'calc(-1 * var(--ModalDialog-padding))',
px: 'var(--ModalDialog-padding)',
overflowY: 'scroll'
}}>
{children} {children}
{mangas?.map(manga => <MangaCard key={manga.key} manga={manga} />)} {mangas?.map(manga => (
<MangaCard key={manga.key} manga={manga}>
<MangaDownloadDialog manga={manga} />
<MangaMerge manga={manga} />
</MangaCard>
))}
</Stack> </Stack>
); );

View File

@@ -0,0 +1,38 @@
import {ReactNode, useContext, useEffect, useState} from "react";
import {Manga} from "../../apiClient/data-contracts.ts";
import Drawer from "@mui/joy/Drawer";
import ModalClose from "@mui/joy/ModalClose";
import {ApiContext} from "../../apiClient/ApiContext.tsx";
import {MangaCard} from "./MangaCard.tsx";
import {Button} from "@mui/joy";
export default function ({manga} : {manga : Manga | undefined}) : ReactNode {
const Api = useContext(ApiContext);
const [similar, setSimilar] = useState<Manga[]>([]);
const [open, setOpen] = useState<boolean>(false);
useEffect(()=> {
if (manga === undefined || !open)
return;
Api.queryMangaSimilarNameList(manga.key as string).then(response => {
if (response.ok)
Api.mangaWithIDsCreate(response.data).then(response => {
if (response.ok)
setSimilar(response.data);
});
});
}, [open]);
return (
<>
<Button onClick={_ => setOpen(true)}>
Merge
</Button>
<Drawer open={open} onClose={() => setOpen(false)} anchor={"bottom"}>
<ModalClose />
{similar.map(manga => <MangaCard manga={manga}></MangaCard>)}
</Drawer>
</>
);
}

View File

@@ -1,12 +1,15 @@
import {Dispatch, KeyboardEventHandler, ReactNode, useContext, useState} from "react"; import {Dispatch, KeyboardEventHandler, ReactNode, useContext, useState} from "react";
import { import {
Badge, Button, Badge,
Button,
Card, Card,
CardContent, CardContent,
CardCover, CardCover, Chip,
Input, Input,
Modal, Modal,
ModalDialog, Option, Select, ModalDialog,
Option,
Select,
Step, Step,
StepIndicator, StepIndicator,
Stepper, Stepper,
@@ -17,6 +20,7 @@ import {MangaConnectorContext} from "../App.tsx";
import {Manga, MangaConnector} from "../apiClient/data-contracts.ts"; import {Manga, MangaConnector} from "../apiClient/data-contracts.ts";
import MangaList from "./Mangas/MangaList.tsx"; import MangaList from "./Mangas/MangaList.tsx";
import {ApiContext} from "../apiClient/ApiContext.tsx"; import {ApiContext} from "../apiClient/ApiContext.tsx";
import {LoadingState, StateColor, StateIndicator} from "./Loading.tsx";
export default function () : ReactNode { export default function () : ReactNode {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -46,6 +50,8 @@ function SearchDialog ({open, setOpen} : {open: boolean, setOpen: Dispatch<boole
const [selectedMangaConnector, setSelectedMangaConnector] = useState<MangaConnector | undefined>(undefined); const [selectedMangaConnector, setSelectedMangaConnector] = useState<MangaConnector | undefined>(undefined);
const [searchTerm, setSearchTerm] = useState<string>(); const [searchTerm, setSearchTerm] = useState<string>();
const [searchResults, setSearchResults] = useState<Manga[]>([]); const [searchResults, setSearchResults] = useState<Manga[]>([]);
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
const doTheSearch = () => { const doTheSearch = () => {
if (searchTerm === undefined || searchTerm.length < 1) if (searchTerm === undefined || searchTerm.length < 1)
@@ -53,18 +59,26 @@ function SearchDialog ({open, setOpen} : {open: boolean, setOpen: Dispatch<boole
if (!isUrl(searchTerm) && selectedMangaConnector === undefined) if (!isUrl(searchTerm) && selectedMangaConnector === undefined)
return; return;
setLoadingState(LoadingState.loading);
if (isUrl(searchTerm)) if (isUrl(searchTerm))
Api.searchUrlCreate(searchTerm) Api.searchUrlCreate(searchTerm)
.then(response => { .then(response => {
if (response.ok) if (response.ok){
setSearchResults([response.data]); setSearchResults([response.data]);
}); setLoadingState(LoadingState.success);
}else
setLoadingState(LoadingState.failure);
}).catch(() => setLoadingState(LoadingState.failure));
else else
Api.searchDetail(selectedMangaConnector!.name, searchTerm) Api.searchDetail(selectedMangaConnector!.name, searchTerm)
.then(response => { .then(response => {
if(response.ok) if(response.ok){
setSearchResults(response.data); setSearchResults(response.data);
}); setLoadingState(LoadingState.success);
}else
setLoadingState(LoadingState.failure);
}).catch(() => setLoadingState(LoadingState.failure));
} }
const isUrl = (url: string) => { const isUrl = (url: string) => {
@@ -89,7 +103,9 @@ function SearchDialog ({open, setOpen} : {open: boolean, setOpen: Dispatch<boole
<Stepper orientation={"vertical"}> <Stepper orientation={"vertical"}>
<Step indicator={<StepIndicator>1</StepIndicator>}> <Step indicator={<StepIndicator>1</StepIndicator>}>
<Typography>Connector</Typography> <Typography>Connector</Typography>
<Select onChange={(_, v) => setSelectedMangaConnector(v as MangaConnector)}> <Select
disabled={loadingState == LoadingState.loading}
onChange={(_, v) => setSelectedMangaConnector(v as MangaConnector)}>
{mangaConnectors?.map(con => ( {mangaConnectors?.map(con => (
<Option value={con}> <Option value={con}>
<Typography><img src={con.iconUrl} style={{maxHeight: "var(--Icon-fontSize)"}} />{con.name}</Typography> <Typography><img src={con.iconUrl} style={{maxHeight: "var(--Icon-fontSize)"}} />{con.name}</Typography>
@@ -99,13 +115,20 @@ function SearchDialog ({open, setOpen} : {open: boolean, setOpen: Dispatch<boole
</Step> </Step>
<Step indicator={<StepIndicator>2</StepIndicator>}> <Step indicator={<StepIndicator>2</StepIndicator>}>
<Typography>Search</Typography> <Typography>Search</Typography>
<Input onKeyDown={keyDownCheck} <Input
onChange={(e) => setSearchTerm(e.currentTarget.value)} disabled={loadingState == LoadingState.loading}
endDecorator={<Button onClick={doTheSearch}>Search</Button>} onKeyDown={keyDownCheck}
onChange={(e) => setSearchTerm(e.currentTarget.value)}
endDecorator={<Button
disabled={loadingState == LoadingState.loading}
onClick={doTheSearch}
endDecorator={StateIndicator(loadingState)}
color={StateColor(loadingState)}
>Search</Button>}
/> />
</Step> </Step>
<Step indicator={<StepIndicator>3</StepIndicator>}> <Step indicator={<StepIndicator>3</StepIndicator>}>
<Typography>Result</Typography> <Typography>Result <Chip>{searchResults.length}</Chip></Typography>
<MangaList mangas={searchResults} /> <MangaList mangas={searchResults} />
</Step> </Step>
</Stepper> </Stepper>

View File

@@ -2,7 +2,6 @@ import {ReactNode, useContext, useState} from "react";
import { ApiContext } from "../../apiClient/ApiContext"; import { ApiContext } from "../../apiClient/ApiContext";
import { import {
Button, Button,
CircularProgress,
Input, Input,
Modal, Modal,
ModalDialog, ModalDialog,
@@ -14,14 +13,7 @@ import {
} from "@mui/joy"; } from "@mui/joy";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import {GotifyRecord, NtfyRecord, PushoverRecord} from "../../apiClient/data-contracts.ts"; import {GotifyRecord, NtfyRecord, PushoverRecord} from "../../apiClient/data-contracts.ts";
import {Close, Done} from "@mui/icons-material"; import {LoadingState, StateColor, StateIndicator} from "../Loading.tsx";
enum LoadingState {
none,
loading,
success,
failure
}
export default function ({open, setOpen} : {open: boolean, setOpen: (open: boolean) => void}) { export default function ({open, setOpen} : {open: boolean, setOpen: (open: boolean) => void}) {
@@ -45,28 +37,6 @@ export default function ({open, setOpen} : {open: boolean, setOpen: (open: boole
} }
function NotificationConnectorTab({ value, children, add, state }: { value: string, children: ReactNode, add: (data: any) => void, state: LoadingState }) { function NotificationConnectorTab({ value, children, add, state }: { value: string, children: ReactNode, add: (data: any) => void, state: LoadingState }) {
const StateIndicator = (state : LoadingState) : ReactNode => {
switch (state) {
case LoadingState.loading:
return (<CircularProgress />);
case LoadingState.failure:
return (<Close />);
case LoadingState.success:
return (<Done />);
default: return null;
}
}
// @ts-ignore
const StateColor = (state : LoadingState) => {
switch (state) {
case LoadingState.failure:
return "danger";
case LoadingState.success:
return "success";
default: return undefined;
}
}
const IsLoading = (state : LoadingState) : boolean => state === LoadingState.loading; const IsLoading = (state : LoadingState) : boolean => state === LoadingState.loading;

View File

@@ -0,0 +1,33 @@
import {Button} from "@mui/joy";
import {SettingsItem} from "./Settings.tsx";
import {useContext, useState} from "react";
import {ApiContext} from "../../apiClient/ApiContext.tsx";
import {LoadingState, StateColor, StateIndicator} from "../Loading.tsx";
export default function () {
const Api = useContext(ApiContext);
const [unusedMangaState, setUnusedMangaState] = useState(LoadingState.none);
const cleanUnusedManga = () => {
setUnusedMangaState(LoadingState.loading);
Api.maintenanceCleanupNoDownloadMangaCreate()
.then(r => {
if (r.ok)
setUnusedMangaState(LoadingState.success);
else
setUnusedMangaState(LoadingState.failure);
}).catch(_ => setUnusedMangaState(LoadingState.failure));
}
return (
<SettingsItem title={"Maintenance"}>
<Button
disabled={unusedMangaState == LoadingState.loading}
color={StateColor(unusedMangaState)}
endDecorator={StateIndicator(unusedMangaState)}
onClick={cleanUnusedManga}>Cleanup unused Manga</Button>
</SettingsItem>
);
}

View File

@@ -19,6 +19,7 @@ import ImageCompression from "./ImageCompression.tsx";
import FlareSolverr from "./FlareSolverr.tsx"; import FlareSolverr from "./FlareSolverr.tsx";
import DownloadLanguage from "./DownloadLanguage.tsx"; import DownloadLanguage from "./DownloadLanguage.tsx";
import ChapterNamingScheme from "./ChapterNamingScheme.tsx"; import ChapterNamingScheme from "./ChapterNamingScheme.tsx";
import Maintenance from "./Maintenance.tsx";
export const SettingsContext = createContext<TrangaSettings|undefined>(undefined); export const SettingsContext = createContext<TrangaSettings|undefined>(undefined);
@@ -73,6 +74,7 @@ export default function Settings({setApiUri} : {setApiUri: Dispatch<React.SetSta
<DownloadLanguage /> <DownloadLanguage />
<ChapterNamingScheme /> <ChapterNamingScheme />
<NotificationConnectors /> <NotificationConnectors />
<Maintenance />
</AccordionGroup> </AccordionGroup>
</DialogContent> </DialogContent>
</ModalDialog> </ModalDialog>

View File

@@ -0,0 +1,32 @@
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
import { HttpClient, RequestParams } from "./http-client";
export class CleanupNoDownloadManga<
SecurityDataType = unknown,
> extends HttpClient<SecurityDataType> {
/**
* No description
*
* @tags Maintenance
* @name CleanupNoDownloadMangaCreate
* @summary Removes all API.Schema.MangaContext.Manga not marked for Download on any API.MangaConnectors.MangaConnector
* @request POST:/CleanupNoDownloadManga
*/
cleanupNoDownloadMangaCreate = (params: RequestParams = {}) =>
this.request<void, string>({
path: `/CleanupNoDownloadManga`,
method: "POST",
...params,
});
}

View File

@@ -204,6 +204,20 @@ export class V2<
method: "DELETE", method: "DELETE",
...params, ...params,
}); });
/**
* No description
*
* @tags Maintenance
* @name MaintenanceCleanupNoDownloadMangaCreate
* @summary Removes all API.Schema.MangaContext.Manga not marked for Download on any API.MangaConnectors.MangaConnector
* @request POST:/v2/Maintenance/CleanupNoDownloadManga
*/
maintenanceCleanupNoDownloadMangaCreate = (params: RequestParams = {}) =>
this.request<void, string>({
path: `/v2/Maintenance/CleanupNoDownloadManga`,
method: "POST",
...params,
});
/** /**
* No description * No description
* *
@@ -255,6 +269,7 @@ export class V2<
* *
* @tags Manga * @tags Manga
* @name MangaDelete * @name MangaDelete
* @summary Delete API.Schema.MangaContext.Manga with MangaId
* @request DELETE:/v2/Manga/{MangaId} * @request DELETE:/v2/Manga/{MangaId}
*/ */
mangaDelete = (mangaId: string, params: RequestParams = {}) => mangaDelete = (mangaId: string, params: RequestParams = {}) =>
@@ -863,6 +878,21 @@ export class V2<
format: "json", format: "json",
...params, ...params,
}); });
/**
* No description
*
* @tags Query
* @name QueryMangaSimilarNameList
* @summary Returns API.Schema.MangaContext.Manga with names similar to API.Schema.MangaContext.Manga (identified by MangaId
* @request GET:/v2/Query/Manga/{MangaId}/SimilarName
*/
queryMangaSimilarNameList = (mangaId: string, params: RequestParams = {}) =>
this.request<string[], ProblemDetails>({
path: `/v2/Query/Manga/${mangaId}/SimilarName`,
method: "GET",
format: "json",
...params,
});
/** /**
* No description * No description
* *