Use new API layout with MinimalManga

This commit is contained in:
2025-09-02 00:46:57 +02:00
parent 2f07cc61fe
commit 9846ffb789
18 changed files with 306 additions and 225 deletions

View File

@@ -10,29 +10,56 @@ import {
FileLibrary, FileLibrary,
Manga, Manga,
MangaConnector, MangaConnector,
MinimalManga,
} from "./apiClient/data-contracts.ts"; } from "./apiClient/data-contracts.ts";
import Search from "./Components/Search.tsx"; import Search from "./Components/Search.tsx";
import { Typography } from "@mui/joy"; import { Typography } from "@mui/joy";
import Workers from "./Components/WorkerModal/Workers.tsx"; import Workers from "./Components/WorkerModal/Workers.tsx";
export const MangaConnectorContext = createContext<MangaConnector[]>([]); const apiUri =
export const MangaContext = createContext<Manga[]>([]);
export const FileLibraryContext = createContext<FileLibrary[]>([]);
export default function App() {
const apiUriStr =
localStorage.getItem("apiUri") ?? localStorage.getItem("apiUri") ??
window.location.href.substring(0, window.location.href.lastIndexOf("/")) + window.location.href.substring(0, window.location.href.lastIndexOf("/")) +
"/api"; "/api";
const [apiUri, setApiUri] = useState<string>(apiUriStr); localStorage.setItem("apiUri", apiUri);
const [Api, setApi] = useState<V2>( const Api = new V2({ baseUrl: apiUri });
new V2({
baseUrl: apiUri,
}),
);
const manga: Manga[] = [];
const promises: Map<string, Promise<Manga | undefined>> = new Map();
const getManga = async (key: string): Promise<Manga | undefined> => {
let result = manga.find((m) => m.key === key);
if (result) return result;
if (promises.has(key)) return promises.get(key);
const newPromise = retrieveManga(key);
promises.set(key, newPromise);
return newPromise;
};
const retrieveManga = async (key: string): Promise<Manga | undefined> => {
return Api.mangaDetail(key).then((response) => {
if (response.ok) {
manga.push(response.data);
return response.data;
}
return undefined;
});
};
export const MangaConnectorContext = createContext<MangaConnector[]>([]);
export const MangaContext = createContext<{
getManga: (key: string) => Promise<Manga | undefined>;
}>({
getManga,
});
export const FileLibraryContext = createContext<FileLibrary[]>([]);
const updateApiUri = (uri: string) => {
localStorage.setItem("apiUri", uri);
window.location.reload();
};
export default function App() {
const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]); const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]);
const [manga, setManga] = useState<Manga[]>([]); const [downloadingManga, setDownloadingManga] = useState<MinimalManga[]>([]);
const [fileLibraries, setFileLibraries] = useState<FileLibrary[]>([]); const [fileLibraries, setFileLibraries] = useState<FileLibrary[]>([]);
useEffect(() => { useEffect(() => {
@@ -45,33 +72,23 @@ export default function App() {
}); });
Api.mangaDownloadingList().then((response) => { Api.mangaDownloadingList().then((response) => {
if (response.ok) setManga(response.data); if (response.ok) setDownloadingManga(response.data);
}); });
}, []); }, []);
useEffect(() => {
localStorage.setItem("apiUri", apiUri);
if (Api.baseUrl != apiUri)
setApi(
new V2({
baseUrl: apiUri,
}),
);
}, [apiUri]);
return ( return (
<ApiContext.Provider value={Api}> <ApiContext.Provider value={Api}>
<FileLibraryContext value={fileLibraries}> <FileLibraryContext value={fileLibraries}>
<MangaConnectorContext.Provider value={mangaConnectors}> <MangaConnectorContext.Provider value={mangaConnectors}>
<MangaContext.Provider value={manga}> <MangaContext.Provider value={{ getManga }}>
{Api ? ( {Api ? (
<Sheet className={"app"}> <Sheet className={"app"}>
<Header> <Header>
<Settings setApiUri={setApiUri} /> <Settings setApiUri={updateApiUri} />
<Workers /> <Workers />
</Header> </Header>
<Sheet className={"app-content"}> <Sheet className={"app-content"}>
<MangaList mangas={manga}> <MangaList manga={downloadingManga}>
<Search /> <Search />
</MangaList> </MangaList>
</Sheet> </Sheet>

View File

@@ -57,7 +57,12 @@ export default function MangaConnectorLink({
} }
> >
<Link href={MangaConnectorId.websiteUrl as string}> <Link href={MangaConnectorId.websiteUrl as string}>
<img ref={imageRef} src={mangaConnector?.iconUrl} style={imageStyle} /> <img
ref={imageRef}
src={mangaConnector?.iconUrl}
style={imageStyle}
className={"manga-card-badge-icon"}
/>
{printName ? <Typography>{mangaConnector?.name}</Typography> : null} {printName ? <Typography>{mangaConnector?.name}</Typography> : null}
</Link> </Link>
</Tooltip> </Tooltip>

View File

@@ -12,12 +12,13 @@ import {
Tooltip, Tooltip,
Typography, Typography,
} from "@mui/joy"; } from "@mui/joy";
import { Manga } from "../../apiClient/data-contracts.ts"; import { Manga, MinimalManga } from "../../apiClient/data-contracts.ts";
import { import {
Dispatch, Dispatch,
ReactNode, ReactNode,
SetStateAction, SetStateAction,
useContext, useContext,
useEffect,
useState, useState,
} from "react"; } from "react";
import "./MangaCard.css"; import "./MangaCard.css";
@@ -28,18 +29,11 @@ import MarkdownPreview from "@uiw/react-markdown-preview";
import { MangaContext } from "../../App.tsx"; import { MangaContext } from "../../App.tsx";
import { MangaConnectorLinkFromId } from "../MangaConnectorLink.tsx"; import { MangaConnectorLinkFromId } from "../MangaConnectorLink.tsx";
export function MangaCardFromId({ mangaId }: { mangaId: string }) {
const mangas = useContext(MangaContext);
const manga = mangas.find((manga) => manga.key === mangaId);
return <MangaCard manga={manga} />;
}
export function MangaCard({ export function MangaCard({
manga, manga,
children, children,
}: { }: {
manga: Manga | undefined; manga: MinimalManga | undefined;
children?: ReactNode; children?: ReactNode;
}) { }) {
if (manga === undefined) return PlaceHolderCard(); if (manga === undefined) return PlaceHolderCard();
@@ -50,14 +44,14 @@ export function MangaCard({
<MangaConnectorBadge manga={manga}> <MangaConnectorBadge manga={manga}>
<Card className={"manga-card"} onClick={() => setOpen(true)}> <Card className={"manga-card"} onClick={() => setOpen(true)}>
<CardCover className={"manga-cover"}> <CardCover className={"manga-cover"}>
<MangaCover manga={manga} /> <MangaCover mangaId={manga?.key} />
</CardCover> </CardCover>
<CardCover className={"manga-cover-blur"} /> <CardCover className={"manga-cover-blur"} />
<CardContent className={"manga-content"}> <CardContent className={"manga-content"}>
<Typography level={"title-lg"}>{manga?.name}</Typography> <Typography level={"title-lg"}>{manga?.name}</Typography>
</CardContent> </CardContent>
</Card> </Card>
<MangaModal manga={manga} open={open} setOpen={setOpen}> <MangaModal minimalManga={manga} open={open} setOpen={setOpen}>
{children} {children}
</MangaModal> </MangaModal>
</MangaConnectorBadge> </MangaConnectorBadge>
@@ -65,16 +59,22 @@ export function MangaCard({
} }
export function MangaModal({ export function MangaModal({
manga, minimalManga,
open, open,
setOpen, setOpen,
children, children,
}: { }: {
manga: Manga | undefined; minimalManga: MinimalManga;
open: boolean; open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>; setOpen: Dispatch<SetStateAction<boolean>>;
children?: ReactNode; children?: ReactNode;
}) { }) {
const { getManga } = useContext(MangaContext);
const [manga, setManga] = useState<Manga>();
useEffect(() => {
getManga(minimalManga.key).then(setManga);
}, []);
return ( return (
<Modal open={open} onClose={() => setOpen(false)} className={"manga-modal"}> <Modal open={open} onClose={() => setOpen(false)} className={"manga-modal"}>
<ModalDialog style={{ width: "100%" }}> <ModalDialog style={{ width: "100%" }}>
@@ -89,12 +89,12 @@ export function MangaModal({
} }
> >
<Typography level={"h4"} width={"fit-content"}> <Typography level={"h4"} width={"fit-content"}>
{manga?.name} {manga?.name ?? minimalManga.name}
</Typography> </Typography>
</Tooltip> </Tooltip>
<Stack direction={"row"} spacing={2}> <Stack direction={"row"} spacing={2}>
<Box key={"Cover"} className={"manga-card"}> <Box key={"Cover"} className={"manga-card"}>
<MangaCover manga={manga} /> <MangaCover mangaId={minimalManga.key} />
</Box> </Box>
<Stack <Stack
key={"Description"} key={"Description"}
@@ -125,8 +125,12 @@ export function MangaModal({
</Stack> </Stack>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<MarkdownPreview <MarkdownPreview
source={manga?.description} source={manga?.description ?? "Loading..."}
style={{ background: "transparent" }} style={{
background: "transparent",
maxHeight: "50vh",
overflowY: "auto",
}}
/> />
</Box> </Box>
<Stack <Stack
@@ -134,7 +138,7 @@ export function MangaModal({
spacing={2} spacing={2}
direction={"row"} direction={"row"}
> >
{children} {manga ? children : null}
</Stack> </Stack>
</Stack> </Stack>
</Stack> </Stack>
@@ -156,10 +160,10 @@ function PlaceHolderCard() {
); );
} }
function MangaCover({ manga }: { manga: Manga | undefined }) { function MangaCover({ mangaId }: { mangaId?: string }) {
const api = useContext(ApiContext); const api = useContext(ApiContext);
const uri = manga const uri = mangaId
? `${api.baseUrl}/v2/Manga/${manga?.key}/Cover` ? `${api.baseUrl}/v2/Manga/${mangaId}/Cover`
: "blahaj.png"; : "blahaj.png";
return ( return (

View File

@@ -1,20 +1,20 @@
import { Badge } from "@mui/joy"; import { Badge } from "@mui/joy";
import { Manga } from "../../apiClient/data-contracts.ts"; import { MinimalManga } from "../../apiClient/data-contracts.ts";
import { ReactElement } from "react"; import { ReactElement } from "react";
import "./MangaCard.css"; import "./MangaCard.css";
import { MangaConnectorLinkFromId } from "../MangaConnectorLink.tsx"; import MangaConnectorLink from "../MangaConnectorLink.tsx";
export default function MangaConnectorBadge({ export default function MangaConnectorBadge({
manga, manga,
children, children,
}: { }: {
manga: Manga; manga: MinimalManga;
children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined; children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined;
}) { }) {
return ( return (
<Badge <Badge
badgeContent={manga.mangaConnectorIdsIds?.map((id) => ( badgeContent={manga.mangaConnectorIds?.map((id) => (
<MangaConnectorLinkFromId key={id} MangaConnectorIdId={id} /> <MangaConnectorLink key={id.key} MangaConnectorId={id} />
))} ))}
> >
{children} {children}

View File

@@ -12,12 +12,17 @@ import Drawer from "@mui/joy/Drawer";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import { MangaConnectorLinkFromId } from "../MangaConnectorLink.tsx"; import { MangaConnectorLinkFromId } from "../MangaConnectorLink.tsx";
import Sheet from "@mui/joy/Sheet"; import Sheet from "@mui/joy/Sheet";
import { FileLibraryContext } from "../../App.tsx"; import { FileLibraryContext, MangaContext } from "../../App.tsx";
import { ApiContext } from "../../apiClient/ApiContext.tsx"; import { ApiContext } from "../../apiClient/ApiContext.tsx";
import { LoadingState, StateIndicator } from "../Loading.tsx"; import { LoadingState, StateIndicator } from "../Loading.tsx";
export default function ({ manga }: { manga: Manga }): ReactNode { export default function ({ mangaId }: { mangaId: string }): ReactNode {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { getManga } = useContext(MangaContext);
const [manga, setManga] = useState<Manga>();
useEffect(() => {
getManga(mangaId).then(setManga);
}, []);
return ( return (
<> <>
@@ -32,7 +37,7 @@ function DownloadDrawer({
open, open,
setOpen, setOpen,
}: { }: {
manga: Manga; manga: Manga | undefined;
open: boolean; open: boolean;
setOpen: Dispatch<boolean>; setOpen: Dispatch<boolean>;
}): ReactNode { }): ReactNode {
@@ -40,7 +45,8 @@ function DownloadDrawer({
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const onLibraryChange = (_: any, value: {} | null) => { const onLibraryChange = (_: any, value: {} | null) => {
if (value === undefined) return; if (!value) return;
if (!manga) return;
Api.mangaChangeLibraryCreate(manga.key as string, value as string); Api.mangaChangeLibraryCreate(manga.key as string, value as string);
}; };
@@ -52,7 +58,7 @@ function DownloadDrawer({
<Select <Select
placeholder={"Library"} placeholder={"Library"}
onChange={onLibraryChange} onChange={onLibraryChange}
value={manga.libraryId} value={manga?.libraryId}
> >
{fileLibraries?.map((library) => ( {fileLibraries?.map((library) => (
<Option value={library.key} key={library.key}> <Option value={library.key} key={library.key}>
@@ -63,7 +69,7 @@ function DownloadDrawer({
</Select> </Select>
<Typography>Download from:</Typography> <Typography>Download from:</Typography>
<Stack> <Stack>
{manga.mangaConnectorIdsIds?.map((id) => ( {manga?.mangaConnectorIdsIds?.map((id) => (
<DownloadCheckBox key={id} mangaConnectorIdId={id} /> <DownloadCheckBox key={id} mangaConnectorIdId={id} />
))} ))}
</Stack> </Stack>

View File

@@ -2,15 +2,15 @@ import { ReactNode } from "react";
import { MangaCard } from "./MangaCard.tsx"; 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 { MinimalManga } from "../../apiClient/data-contracts.ts";
import MangaDownloadDialog from "./MangaDownloadDialog.tsx"; import MangaDownloadDialog from "./MangaDownloadDialog.tsx";
import MangaMerge from "./MangaMerge.tsx"; import MangaMerge from "./MangaMerge.tsx";
export default function MangaList({ export default function MangaList({
mangas, manga,
children, children,
}: { }: {
mangas: Manga[]; manga: MinimalManga[];
children?: ReactNode; children?: ReactNode;
}) { }) {
return ( return (
@@ -27,10 +27,10 @@ export default function MangaList({
}} }}
> >
{children} {children}
{mangas?.map((manga) => ( {manga?.map((minimalManga) => (
<MangaCard key={manga.key} manga={manga}> <MangaCard key={minimalManga.key} manga={minimalManga}>
<MangaDownloadDialog manga={manga} /> <MangaDownloadDialog mangaId={minimalManga.key} />
<MangaMerge manga={manga} /> <MangaMerge manga={minimalManga} />
</MangaCard> </MangaCard>
))} ))}
</Stack> </Stack>

View File

@@ -1,5 +1,5 @@
import { ReactNode, useContext, useEffect, useState } from "react"; import { ReactNode, useContext, useEffect, useState } from "react";
import { Manga } from "../../apiClient/data-contracts.ts"; import { Manga, MinimalManga } from "../../apiClient/data-contracts.ts";
import Drawer from "@mui/joy/Drawer"; import Drawer from "@mui/joy/Drawer";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import { ApiContext } from "../../apiClient/ApiContext.tsx"; import { ApiContext } from "../../apiClient/ApiContext.tsx";
@@ -16,10 +16,14 @@ import {
import { KeyboardDoubleArrowRight, Warning } from "@mui/icons-material"; import { KeyboardDoubleArrowRight, Warning } from "@mui/icons-material";
import { LoadingState, StateIndicator } from "../Loading.tsx"; import { LoadingState, StateIndicator } from "../Loading.tsx";
export default function ({ manga }: { manga: Manga | undefined }): ReactNode { export default function ({
manga,
}: {
manga: MinimalManga | undefined;
}): ReactNode {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [similar, setSimilar] = useState<Manga[]>([]); const [similar, setSimilar] = useState<Manga[]>();
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
@@ -34,7 +38,7 @@ export default function ({ manga }: { manga: Manga | undefined }): ReactNode {
const exit = (manga: Manga) => { const exit = (manga: Manga) => {
setOpen(false); setOpen(false);
setSimilar(similar.filter((m) => m.key != manga.key)); setSimilar(similar?.filter((m) => m.key != manga.key));
}; };
return ( return (
@@ -47,8 +51,10 @@ export default function ({ manga }: { manga: Manga | undefined }): ReactNode {
anchor={"bottom"} anchor={"bottom"}
> >
<ModalClose /> <ModalClose />
<Typography>Merge targets: {similar?.length ?? 0}</Typography>
<Stack direction={"row"} spacing={2} flexWrap={"wrap"} useFlexGap> <Stack direction={"row"} spacing={2} flexWrap={"wrap"} useFlexGap>
{similar.map((similarManga) => ( {similar
? similar?.map((similarManga) => (
<MangaCard manga={similarManga}> <MangaCard manga={similarManga}>
<ConfirmationModal <ConfirmationModal
manga={manga as Manga} manga={manga as Manga}
@@ -56,7 +62,8 @@ export default function ({ manga }: { manga: Manga | undefined }): ReactNode {
exit={() => exit(similarManga)} exit={() => exit(similarManga)}
/> />
</MangaCard> </MangaCard>
))} ))
: "Loading..."}
</Stack> </Stack>
</Drawer> </Drawer>
</> </>

View File

@@ -24,7 +24,7 @@ import {
} from "@mui/joy"; } from "@mui/joy";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import { MangaConnectorContext } from "../App.tsx"; import { MangaConnectorContext } from "../App.tsx";
import { Manga, MangaConnector } from "../apiClient/data-contracts.ts"; import { MangaConnector, MinimalManga } 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"; import { LoadingState, StateColor, StateIndicator } from "./Loading.tsx";
@@ -67,7 +67,7 @@ function SearchDialog({
MangaConnector | undefined MangaConnector | undefined
>(undefined); >(undefined);
const [searchTerm, setSearchTerm] = useState<string>(); const [searchTerm, setSearchTerm] = useState<string>();
const [searchResults, setSearchResults] = useState<Manga[]>([]); const [searchResults, setSearchResults] = useState<MinimalManga[]>([]);
const [loadingState, setLoadingState] = useState<LoadingState>( const [loadingState, setLoadingState] = useState<LoadingState>(
LoadingState.none, LoadingState.none,
@@ -166,7 +166,7 @@ function SearchDialog({
<Typography> <Typography>
Result <Chip>{searchResults.length}</Chip> Result <Chip>{searchResults.length}</Chip>
</Typography> </Typography>
<MangaList mangas={searchResults} /> <MangaList manga={searchResults} />
</Step> </Step>
</Stepper> </Stepper>
</ModalDialog> </ModalDialog>

View File

@@ -1,14 +1,14 @@
import {SettingsItem} from "./Settings.tsx"; import { SettingsItem } from "./Settings.tsx";
import ImageCompression from "./ImageCompression.tsx"; import ImageCompression from "./ImageCompression.tsx";
import DownloadLanguage from "./DownloadLanguage.tsx"; import DownloadLanguage from "./DownloadLanguage.tsx";
import ChapterNamingScheme from "./ChapterNamingScheme.tsx"; import ChapterNamingScheme from "./ChapterNamingScheme.tsx";
export default function (){ export default function () {
return ( return (
<SettingsItem title={"Download"}> <SettingsItem title={"Download"}>
<ImageCompression /> <ImageCompression />
<DownloadLanguage /> <DownloadLanguage />
<ChapterNamingScheme /> <ChapterNamingScheme />
</SettingsItem> </SettingsItem>
) );
} }

View File

@@ -1,13 +1,19 @@
import {Modal, ModalDialog, Tab, TabList, Tabs} from "@mui/joy"; import { Modal, ModalDialog, Tab, TabList, Tabs } from "@mui/joy";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import {Dispatch} from "react"; import { Dispatch } from "react";
export default function ({open, setOpen} : {open: boolean, setOpen: Dispatch<boolean>}) { export default function ({
open,
setOpen,
}: {
open: boolean;
setOpen: Dispatch<boolean>;
}) {
return ( return (
<Modal open={open} onClose={() => setOpen(false)}> <Modal open={open} onClose={() => setOpen(false)}>
<ModalDialog> <ModalDialog>
<ModalClose /> <ModalClose />
<Tabs sx={{width:'95%'}} defaultValue={"komga"}> <Tabs sx={{ width: "95%" }} defaultValue={"komga"}>
<TabList> <TabList>
<Tab value={"komga"}>Komga</Tab> <Tab value={"komga"}>Komga</Tab>
<Tab value={"kavita"}>Kavita</Tab> <Tab value={"kavita"}>Kavita</Tab>

View File

@@ -1,10 +1,9 @@
import {Button, Card, Typography} from "@mui/joy"; import { Button, Card, Typography } from "@mui/joy";
import {useState} from "react"; import { useState } from "react";
import ListLibraryConnectors from "./ListLibraryConnectors.tsx"; import ListLibraryConnectors from "./ListLibraryConnectors.tsx";
import AddLibraryConnector from "./AddLibraryConnector.tsx"; import AddLibraryConnector from "./AddLibraryConnector.tsx";
export default function (){ export default function () {
const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false); const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false);
return ( return (
@@ -12,7 +11,10 @@ export default function (){
<Typography>Library Connectors</Typography> <Typography>Library Connectors</Typography>
<Button onClick={() => setAddDialogOpen(true)}>Add</Button> <Button onClick={() => setAddDialogOpen(true)}>Add</Button>
<ListLibraryConnectors /> <ListLibraryConnectors />
<AddLibraryConnector open={addDialogOpen} setOpen={() => setAddDialogOpen(false)} /> <AddLibraryConnector
open={addDialogOpen}
setOpen={() => setAddDialogOpen(false)}
/>
</Card> </Card>
); );
} }

View File

@@ -1,31 +1,34 @@
import {useContext, useEffect, useState} from "react"; import { useContext, useEffect, useState } from "react";
import {ApiContext} from "../../../apiClient/ApiContext.tsx"; import { ApiContext } from "../../../apiClient/ApiContext.tsx";
import {LibraryConnector} from "../../../apiClient/data-contracts.ts"; import { LibraryConnector } from "../../../apiClient/data-contracts.ts";
import {Card, Chip, Input, Stack} from "@mui/joy"; import { Card, Chip, Input, Stack } from "@mui/joy";
export default function (){ export default function () {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [libraryConnectors, setLibraryConnectors] = useState<LibraryConnector[]>([]); const [libraryConnectors, setLibraryConnectors] = useState<
LibraryConnector[]
>([]);
useEffect(() => { useEffect(() => {
getConnectors(); getConnectors();
}, []); }, []);
const getConnectors = () => { const getConnectors = () => {
Api.libraryConnectorList().then(r => { Api.libraryConnectorList().then((r) => {
if(r.ok) if (r.ok) setLibraryConnectors(r.data);
setLibraryConnectors(r.data); });
})
}; };
return ( return (
<Stack direction={"column"} spacing={1}> <Stack direction={"column"} spacing={1}>
{libraryConnectors.map(c => <LibraryConnectorItem key={c.key} connector={c} />)} {libraryConnectors.map((c) => (
<LibraryConnectorItem key={c.key} connector={c} />
))}
</Stack> </Stack>
); );
} }
function LibraryConnectorItem({connector} : {connector: LibraryConnector}){ function LibraryConnectorItem({ connector }: { connector: LibraryConnector }) {
return ( return (
<Card> <Card>
<Chip>{connector.libraryType}</Chip> <Chip>{connector.libraryType}</Chip>

View File

@@ -1,35 +1,54 @@
import {ApiContext} from "../../../apiClient/ApiContext.tsx"; import { ApiContext } from "../../../apiClient/ApiContext.tsx";
import {useContext, useEffect, useState} from "react"; import { useContext, useEffect, useState } from "react";
import { NotificationConnector } from "../../../apiClient/data-contracts.ts"; import { NotificationConnector } from "../../../apiClient/data-contracts.ts";
import {Card, Chip, Input, Stack, Table, Textarea, Typography} from "@mui/joy"; import {
Card,
Chip,
Input,
Stack,
Table,
Textarea,
Typography,
} from "@mui/joy";
export default function (){ export default function () {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [notificationConnectors, setNotificationConnectors] = useState<NotificationConnector[]>([]); const [notificationConnectors, setNotificationConnectors] = useState<
NotificationConnector[]
>([]);
useEffect(() => { useEffect(() => {
getConnectors(); getConnectors();
}, []); }, []);
const getConnectors = () => { const getConnectors = () => {
Api.notificationConnectorList().then(r => { Api.notificationConnectorList().then((r) => {
if(r.ok) if (r.ok) setNotificationConnectors(r.data);
setNotificationConnectors(r.data); });
})
}; };
return ( return (
<Stack direction={"column"} spacing={1}> <Stack direction={"column"} spacing={1}>
{notificationConnectors.map(c => <NotificationConnectorItem key={c.name} connector={c} />)} {notificationConnectors.map((c) => (
<NotificationConnectorItem key={c.name} connector={c} />
))}
</Stack> </Stack>
); );
} }
function NotificationConnectorItem({connector} : {connector: NotificationConnector}){ function NotificationConnectorItem({
connector,
}: {
connector: NotificationConnector;
}) {
return ( return (
<Card> <Card>
<Typography left={"h2"}>{connector.name}</Typography> <Typography left={"h2"}>{connector.name}</Typography>
<Input disabled startDecorator={<Chip>{connector.httpMethod}</Chip>} value={connector.url} /> <Input
disabled
startDecorator={<Chip>{connector.httpMethod}</Chip>}
value={connector.url}
/>
<Table> <Table>
<thead> <thead>
<tr> <tr>
@@ -38,15 +57,15 @@ function NotificationConnectorItem({connector} : {connector: NotificationConnect
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{Object.entries(connector.headers).map(x => {Object.entries(connector.headers).map((x) => (
<tr key={x[0]}> <tr key={x[0]}>
<td>{x[0]}</td> <td>{x[0]}</td>
<td>{[x[1]]}</td> <td>{[x[1]]}</td>
</tr> </tr>
)} ))}
</tbody> </tbody>
</Table> </Table>
<Textarea disabled value={connector.body}/> <Textarea disabled value={connector.body} />
</Card> </Card>
); );
} }

View File

@@ -1,11 +1,8 @@
import {SettingsItem} from "./Settings.tsx"; import { SettingsItem } from "./Settings.tsx";
import FlareSolverr from "./FlareSolverr.tsx"; import FlareSolverr from "./FlareSolverr.tsx";
import LibraryConnectors from "./LibraryConnectors/LibraryConnectors.tsx"; import LibraryConnectors from "./LibraryConnectors/LibraryConnectors.tsx";
export default function(){ export default function () {
return ( return (
<SettingsItem title={"Services"}> <SettingsItem title={"Services"}>
<FlareSolverr /> <FlareSolverr />

View File

@@ -16,7 +16,6 @@ import "./Settings.css";
import * as React from "react"; import * as React from "react";
import { import {
createContext, createContext,
Dispatch,
ReactNode, ReactNode,
useContext, useContext,
useEffect, useEffect,
@@ -38,7 +37,7 @@ export const SettingsContext = createContext<TrangaSettings | undefined>(
export default function Settings({ export default function Settings({
setApiUri, setApiUri,
}: { }: {
setApiUri: Dispatch<React.SetStateAction<string>>; setApiUri: (uri: string) => void;
}) { }) {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [settings, setSettings] = useState<TrangaSettings>(); const [settings, setSettings] = useState<TrangaSettings>();

View File

@@ -1,4 +1,4 @@
import { Dispatch, ReactNode, useContext, useState } from "react"; import { Dispatch, ReactNode, useContext, useEffect, useState } from "react";
import Drawer from "@mui/joy/Drawer"; import Drawer from "@mui/joy/Drawer";
import { Button, Option, Select, Table } from "@mui/joy"; import { Button, Option, Select, Table } from "@mui/joy";
import { BaseWorker } from "../../apiClient/data-contracts.ts"; import { BaseWorker } from "../../apiClient/data-contracts.ts";
@@ -10,11 +10,14 @@ export default function (): ReactNode {
const [workers, setWorkers] = useState<BaseWorker[]>([]); const [workers, setWorkers] = useState<BaseWorker[]>([]);
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
useEffect(() => {
Api.workerList().then((response) => { Api.workerList().then((response) => {
if (response.ok) { if (response.ok) {
setWorkers(response.data); setWorkers(response.data);
} }
}); });
}, []);
return ( return (
<> <>
@@ -52,7 +55,7 @@ function WorkerDrawer({
<tbody> <tbody>
{workers.map((worker) => { {workers.map((worker) => {
return ( return (
<tr> <tr key={worker.key}>
<td>{worker.key}</td> <td>{worker.key}</td>
<td>{worker.allDependenciesFulfilled ? "yes" : "no"}</td> <td>{worker.allDependenciesFulfilled ? "yes" : "no"}</td>
<td> <td>

View File

@@ -1,8 +1,6 @@
/* eslint-disable */ /* eslint-disable */
/* tslint:disable */ /* tslint:disable */
// @ts-nocheck // @ts-nocheck
// noinspection JSValidateJSDoc,JSUnusedGlobalSymbols
/* /*
* --------------------------------------------------------------- * ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
@@ -25,6 +23,7 @@ import {
MangaMangaConnectorId, MangaMangaConnectorId,
MetadataEntry, MetadataEntry,
MetadataSearchResult, MetadataSearchResult,
MinimalManga,
NotificationConnector, NotificationConnector,
NtfyRecord, NtfyRecord,
ProblemDetails, ProblemDetails,
@@ -230,7 +229,7 @@ export class V2<
* @request GET:/v2/Manga * @request GET:/v2/Manga
*/ */
mangaList = (params: RequestParams = {}) => mangaList = (params: RequestParams = {}) =>
this.request<Manga[], void>({ this.request<MinimalManga[], void>({
path: `/v2/Manga`, path: `/v2/Manga`,
method: "GET", method: "GET",
format: "json", format: "json",
@@ -260,7 +259,7 @@ export class V2<
* @request GET:/v2/Manga/Downloading * @request GET:/v2/Manga/Downloading
*/ */
mangaDownloadingList = (params: RequestParams = {}) => mangaDownloadingList = (params: RequestParams = {}) =>
this.request<Manga[], void>({ this.request<MinimalManga[], void>({
path: `/v2/Manga/Downloading`, path: `/v2/Manga/Downloading`,
method: "GET", method: "GET",
format: "json", format: "json",
@@ -943,7 +942,7 @@ export class V2<
query: string, query: string,
params: RequestParams = {}, params: RequestParams = {},
) => ) =>
this.request<Manga[], ProblemDetails | void>({ this.request<MinimalManga[], ProblemDetails | void>({
path: `/v2/Search/${mangaConnectorName}/${query}`, path: `/v2/Search/${mangaConnectorName}/${query}`,
method: "GET", method: "GET",
format: "json", format: "json",
@@ -958,7 +957,7 @@ export class V2<
* @request POST:/v2/Search/Url * @request POST:/v2/Search/Url
*/ */
searchUrlCreate = (data: string, params: RequestParams = {}) => searchUrlCreate = (data: string, params: RequestParams = {}) =>
this.request<Manga, void | ProblemDetails>({ this.request<MinimalManga, void | ProblemDetails>({
path: `/v2/Search/Url`, path: `/v2/Search/Url`,
method: "POST", method: "POST",
body: data, body: data,

View File

@@ -331,6 +331,20 @@ export interface MetadataSearchResult {
coverUrl?: string | null; coverUrl?: string | null;
} }
export interface MinimalManga {
/**
* @minLength 16
* @maxLength 64
*/
key: string;
/** @minLength 1 */
name: string;
/** @minLength 1 */
description: string;
releaseStatus: MangaReleaseStatus;
mangaConnectorIds?: MangaMangaConnectorId[] | null;
}
export interface NotificationConnector { export interface NotificationConnector {
/** /**
* @minLength 0 * @minLength 0