Download Dialog

This commit is contained in:
2025-07-22 16:08:16 +02:00
parent 7b038ad377
commit cd566f01e1
8 changed files with 281 additions and 90 deletions

View File

@@ -6,10 +6,11 @@ import {createContext, useEffect, useState} from "react";
import {V2} from "./apiClient/V2.ts"; import {V2} from "./apiClient/V2.ts";
import { ApiContext } from './apiClient/ApiContext.tsx'; import { ApiContext } from './apiClient/ApiContext.tsx';
import MangaList from "./Components/Mangas/MangaList.tsx"; import MangaList from "./Components/Mangas/MangaList.tsx";
import {Manga, MangaConnector} from "./apiClient/data-contracts.ts"; import {FileLibrary, Manga, MangaConnector} from "./apiClient/data-contracts.ts";
export const MangaConnectorContext = createContext<MangaConnector[]>([]); export const MangaConnectorContext = createContext<MangaConnector[]>([]);
export const MangaContext = createContext<Manga[]>([]); export const MangaContext = createContext<Manga[]>([]);
export const FileLibraryContext = createContext<FileLibrary[]>([]);
export default function App () { export default function App () {
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/api"; const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/api";
@@ -18,6 +19,7 @@ export default function App () {
const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]); const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]);
const [manga, setManga] = useState<Manga[]>([]); const [manga, setManga] = useState<Manga[]>([]);
const [fileLibraries, setFileLibraries] = useState<FileLibrary[]>([]);
useEffect(() => { useEffect(() => {
Api.mangaConnectorList().then(response => { Api.mangaConnectorList().then(response => {
@@ -25,6 +27,11 @@ export default function App () {
setMangaConnectors(response.data); setMangaConnectors(response.data);
}); });
Api.fileLibraryList().then(response => {
if (response.ok)
setFileLibraries(response.data);
})
Api.mangaList().then(response => { Api.mangaList().then(response => {
if (!response.ok) if (!response.ok)
{ {
@@ -48,6 +55,7 @@ export default function App () {
return ( return (
<ApiContext.Provider value={Api}> <ApiContext.Provider value={Api}>
<FileLibraryContext value={fileLibraries}>
<MangaConnectorContext.Provider value={mangaConnectors}> <MangaConnectorContext.Provider value={mangaConnectors}>
<MangaContext.Provider value={manga}> <MangaContext.Provider value={manga}>
<Sheet className={"app"}> <Sheet className={"app"}>
@@ -60,6 +68,7 @@ export default function App () {
</Sheet> </Sheet>
</MangaContext.Provider> </MangaContext.Provider>
</MangaConnectorContext.Provider> </MangaConnectorContext.Provider>
</FileLibraryContext>
</ApiContext.Provider> </ApiContext.Provider>
); );
} }

View File

@@ -0,0 +1,41 @@
import {CSSProperties, ReactNode, useContext, useEffect, useRef, useState} from "react";
import {ChapterMangaConnectorId, MangaConnector, MangaMangaConnectorId} from "../apiClient/data-contracts.ts";
import {Link, Tooltip, Typography} from "@mui/joy";
import {MangaConnectorContext} from "../App.tsx";
import {ApiContext} from "../apiClient/ApiContext.tsx";
export default function MangaConnectorLink({MangaConnectorId, imageStyle} : {MangaConnectorId : MangaMangaConnectorId | ChapterMangaConnectorId, imageStyle? : CSSProperties}) : ReactNode{
const mangaConnectorContext = useContext(MangaConnectorContext);
const [mangaConnector, setMangaConnector] = useState<MangaConnector | undefined>(mangaConnectorContext?.find(c => c.name == MangaConnectorId.mangaConnectorName));
const imageRef = useRef<HTMLImageElement | null>(null);
useEffect(() => {
const connector = mangaConnectorContext?.find(c => c.name == MangaConnectorId.mangaConnectorName);
setMangaConnector(connector);
if (imageRef?.current != null)
imageRef.current.setHTMLUnsafe("<img ref={imageRef} src={mangaConnector?.iconUrl} style={imageStyle}/>");
}, []);
return (
<Tooltip title={<Typography>{MangaConnectorId.mangaConnectorName}: <Link href={MangaConnectorId.websiteUrl as string}>{MangaConnectorId.websiteUrl}</Link></Typography>}>
<Link href={MangaConnectorId.websiteUrl as string}>
<img ref={imageRef} src={mangaConnector?.iconUrl} style={imageStyle}/>
</Link>
</Tooltip>
);
}
export function MangaConnectorLinkFromId({MangaConnectorIdId} : {MangaConnectorIdId: string}) : ReactNode {
const Api = useContext(ApiContext);
const [node, setNode] = useState<ReactNode>(null);
useEffect(() => {
Api.queryMangaMangaConnectorIdDetail(MangaConnectorIdId).then(response => {
if (response.ok)
setNode(<MangaConnectorLink key={response.data.key} MangaConnectorId={response.data} imageStyle={{width: "25px"}}/>);
});
}, []);
return node;
}

View File

@@ -12,13 +12,15 @@ import {
Typography Typography
} from "@mui/joy"; } from "@mui/joy";
import {Manga} from "../../apiClient/data-contracts.ts"; import {Manga} from "../../apiClient/data-contracts.ts";
import {Dispatch, SetStateAction, useContext, useState} from "react"; import {Dispatch, ReactNode, SetStateAction, useContext, useState} from "react";
import "./MangaCard.css"; import "./MangaCard.css";
import MangaConnectorBadge from "./MangaConnectorBadge.tsx"; import MangaConnectorBadge from "./MangaConnectorBadge.tsx";
import ModalClose from "@mui/joy/ModalClose"; 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";
export function MangaCardFromId({mangaId} : {mangaId: string}) { export function MangaCardFromId({mangaId} : {mangaId: string}) {
const mangas = useContext(MangaContext); const mangas = useContext(MangaContext);
@@ -44,12 +46,14 @@ export function MangaCard({manga} : {manga: Manga | undefined}) {
<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 manga={manga} open={open} setOpen={setOpen}>
<MangaDownloadDialog manga={manga} />
</MangaModal>
</MangaConnectorBadge> </MangaConnectorBadge>
); );
} }
function MangaModal({manga, open, setOpen}: {manga: Manga | undefined, open: boolean, setOpen: Dispatch<SetStateAction<boolean>>}) { export function MangaModal({manga, open, setOpen, children}: {manga: Manga | undefined, open: boolean, setOpen: Dispatch<SetStateAction<boolean>>, children?: ReactNode}) {
return ( return (
<Modal open={open} onClose={() => setOpen(false)} className={"manga-modal"}> <Modal open={open} onClose={() => setOpen(false)} className={"manga-modal"}>
@@ -64,14 +68,16 @@ function MangaModal({manga, open, setOpen}: {manga: Manga | undefined, open: boo
</Box> </Box>
<Stack direction={"column"} sx={{width: "calc(100% - 230px)"}}> <Stack direction={"column"} sx={{width: "calc(100% - 230px)"}}>
<Stack direction={"row"} flexWrap={"wrap"} useFlexGap spacing={0.5}> <Stack direction={"row"} flexWrap={"wrap"} useFlexGap spacing={0.5}>
{manga?.mangaConnectorIdsIds?.map((idid) => <MangaConnectorLinkFromId MangaConnectorIdId={idid} />)}
{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>
<MarkdownPreview source={manga?.description}/> <MarkdownPreview source={manga?.description} style={{background: "transparent"}}/>
</Box> </Box>
</Stack> </Stack>
</Stack> </Stack>
{children}
</ModalDialog> </ModalDialog>
</Modal> </Modal>
); );

View File

@@ -1,20 +1,13 @@
import { Badge } from "@mui/joy"; import { Badge } from "@mui/joy";
import {Manga, MangaConnector} from "../../apiClient/data-contracts.ts"; import {Manga} from "../../apiClient/data-contracts.ts";
import {ReactElement, useContext, useEffect, useState} from "react"; import {ReactElement} from "react";
import {MangaConnectorContext} from "../../App.tsx";
import "./MangaCard.css" import "./MangaCard.css"
import {MangaConnectorLinkFromId} from "../MangaConnectorLink.tsx";
export default function MangaConnectorBadge ({manga, children} : {manga: Manga, children? : ReactElement<any, any> | ReactElement<any,any>[] | undefined}) { export default function MangaConnectorBadge ({manga, children} : {manga: Manga, children? : ReactElement<any, any> | ReactElement<any,any>[] | undefined}) {
const context = useContext(MangaConnectorContext);
const [connectors, setConnectors] = useState<MangaConnector[]>([]);
useEffect(() => {
if (context)
setConnectors(context.filter(con => Object.keys(manga.idsOnMangaConnectors??[]).find(name => con.name == name)));
}, []);
return ( return (
<Badge badgeContent={connectors?.map(connector => <img key={connector.name} src={connector.iconUrl} className={"manga-card-badge-icon"} />)}> <Badge badgeContent={manga.mangaConnectorIdsIds?.map(id => <MangaConnectorLinkFromId MangaConnectorIdId={id} />)}>
{children} {children}
</Badge> </Badge>
); );

View File

@@ -0,0 +1,47 @@
import {Manga} from "../../apiClient/data-contracts.ts";
import {Dispatch, ReactNode, useContext, useState} from "react";
import {Button, Checkbox, Option, Select, Stack, Typography} from "@mui/joy";
import Drawer from "@mui/joy/Drawer";
import ModalClose from "@mui/joy/ModalClose";
import {MangaConnectorLinkFromId} from "../MangaConnectorLink.tsx";
import Sheet from "@mui/joy/Sheet";
import {FileLibraryContext} from "../../App.tsx";
export default function ({manga} : {manga: Manga}) : ReactNode{
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Download</Button>
<DownloadDrawer manga={manga} open={open} setOpen={setOpen} />
</>
);
}
function DownloadDrawer({manga, open, setOpen}: {manga: Manga, open: boolean, setOpen: Dispatch<boolean>}): ReactNode {
const fileLibraries = useContext(FileLibraryContext);
return (
<Drawer open={open} onClose={() => setOpen(false)}>
<ModalClose />
<Sheet sx={{width: "calc(95% - 60px)", margin: "30px"}}>
<Typography>Download to Library:</Typography>
<Select placeholder={"Library"}>
{fileLibraries?.map(library => (
<Option value={library.key} key={library.key}><Typography>{library.libraryName}</Typography> <Typography>({library.basePath})</Typography></Option>
))}
</Select>
<Typography>Download from:</Typography>
<Stack>
{manga.mangaConnectorIdsIds?.map(id => <DownloadCheckBox key={id} mangaConnectorIdId={id} />)}
</Stack>
</Sheet>
</Drawer>
);
}
function DownloadCheckBox({mangaConnectorIdId} : {mangaConnectorIdId : string}) : ReactNode {
return (
<Checkbox label={<Typography><MangaConnectorLinkFromId MangaConnectorIdId={mangaConnectorIdId} /></Typography>} />
);
}

View File

@@ -1,4 +1,3 @@
import Drawer from '@mui/joy/Drawer';
import ModalClose from '@mui/joy/ModalClose'; import ModalClose from '@mui/joy/ModalClose';
import { import {
Accordion, Accordion,
@@ -7,7 +6,7 @@ import {
AccordionSummary, Button, ColorPaletteProp, AccordionSummary, Button, ColorPaletteProp,
DialogContent, DialogContent,
DialogTitle, Input, DialogTitle, Input,
Link, Stack Link, Modal, ModalDialog, Stack
} from "@mui/joy"; } from "@mui/joy";
import './Settings.css'; import './Settings.css';
import * as React from "react"; import * as React from "react";
@@ -16,6 +15,7 @@ import {Article} from '@mui/icons-material';
import {TrangaSettings} from "../../apiClient/data-contracts.ts"; import {TrangaSettings} from "../../apiClient/data-contracts.ts";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import {ApiContext} from "../../apiClient/ApiContext.tsx";
import NotificationConnectors from "./AddNotificationConnector.tsx"; import NotificationConnectors from "./AddNotificationConnector.tsx";
import {SxProps} from "@mui/joy/styles/types";
export const SettingsContext = createContext<TrangaSettings>({}); export const SettingsContext = createContext<TrangaSettings>({});
@@ -46,10 +46,16 @@ export default function Settings({setApiUri} : {setApiUri: Dispatch<React.SetSta
const [notificationConnectorsOpen, setNotificationConnectorsOpen] = React.useState(false); const [notificationConnectorsOpen, setNotificationConnectorsOpen] = React.useState(false);
const ModalStyle : SxProps = {
width: "80%",
height: "80%"
}
return ( return (
<SettingsContext value={settings}> <SettingsContext value={settings}>
<Button onClick={() => setOpen(true)}>Settings</Button> <Button onClick={() => setOpen(true)}>Settings</Button>
<Drawer size={"lg"} open={open} onClose={() => setOpen(false)}> <Modal open={open} onClose={() => setOpen(false)}>
<ModalDialog sx={ModalStyle}>
<ModalClose /> <ModalClose />
<DialogTitle>Settings</DialogTitle> <DialogTitle>Settings</DialogTitle>
<DialogContent> <DialogContent>
@@ -74,7 +80,8 @@ export default function Settings({setApiUri} : {setApiUri: Dispatch<React.SetSta
<Link target={"_blank"} href={Api.baseUrl + "/swagger"}><Article />Swagger Doc</Link> <Link target={"_blank"} href={Api.baseUrl + "/swagger"}><Article />Swagger Doc</Link>
</Stack> </Stack>
</DialogContent> </DialogContent>
</Drawer> </ModalDialog>
</Modal>
</SettingsContext> </SettingsContext>
); );
} }

View File

@@ -14,11 +14,13 @@ import {
Author, Author,
BaseWorker, BaseWorker,
Chapter, Chapter,
ChapterMangaConnectorId,
FileLibrary, FileLibrary,
GotifyRecord, GotifyRecord,
LibraryConnector, LibraryConnector,
Manga, Manga,
MangaConnector, MangaConnector,
MangaMangaConnectorId,
MetadataEntry, MetadataEntry,
MetadataSearchResult, MetadataSearchResult,
NotificationConnector, NotificationConnector,
@@ -471,6 +473,36 @@ export class V2<
format: "json", format: "json",
...params, ...params,
}); });
/**
* No description
*
* @tags Manga
* @name MangaWithAuthorIdDetail
* @summary Returns all API.Schema.MangaContext.Manga which where Authored by API.Schema.MangaContext.Author with AuthorId
* @request GET:/v2/Manga/WithAuthorId/{AuthorId}
*/
mangaWithAuthorIdDetail = (authorId: string, params: RequestParams = {}) =>
this.request<Manga[], void>({
path: `/v2/Manga/WithAuthorId/${authorId}`,
method: "GET",
format: "json",
...params,
});
/**
* No description
*
* @tags Manga
* @name MangaWithTagDetail
* @summary Returns all API.Schema.MangaContext.Manga with !:Tag
* @request GET:/v2/Manga/WithTag/{Tag}
*/
mangaWithTagDetail = (tag: string, params: RequestParams = {}) =>
this.request<Manga[], void>({
path: `/v2/Manga/WithTag/${tag}`,
method: "GET",
format: "json",
...params,
});
/** /**
* No description * No description
* *
@@ -783,39 +815,6 @@ export class V2<
format: "json", format: "json",
...params, ...params,
}); });
/**
* No description
*
* @tags Query
* @name QueryMangasWithAuthorIdDetail
* @summary Returns all API.Schema.MangaContext.Manga which where Authored by API.Schema.MangaContext.Author with AuthorId
* @request GET:/v2/Query/Mangas/WithAuthorId/{AuthorId}
*/
queryMangasWithAuthorIdDetail = (
authorId: string,
params: RequestParams = {},
) =>
this.request<Manga[], void>({
path: `/v2/Query/Mangas/WithAuthorId/${authorId}`,
method: "GET",
format: "json",
...params,
});
/**
* No description
*
* @tags Query
* @name QueryMangasWithTagDetail
* @summary Returns all API.Schema.MangaContext.Manga with !:Tag
* @request GET:/v2/Query/Mangas/WithTag/{Tag}
*/
queryMangasWithTagDetail = (tag: string, params: RequestParams = {}) =>
this.request<Manga[], void>({
path: `/v2/Query/Mangas/WithTag/${tag}`,
method: "GET",
format: "json",
...params,
});
/** /**
* No description * No description
* *
@@ -825,12 +824,48 @@ export class V2<
* @request GET:/v2/Query/Chapter/{ChapterId} * @request GET:/v2/Query/Chapter/{ChapterId}
*/ */
queryChapterDetail = (chapterId: string, params: RequestParams = {}) => queryChapterDetail = (chapterId: string, params: RequestParams = {}) =>
this.request<Chapter, void>({ this.request<Chapter, ProblemDetails>({
path: `/v2/Query/Chapter/${chapterId}`, path: `/v2/Query/Chapter/${chapterId}`,
method: "GET", method: "GET",
format: "json", format: "json",
...params, ...params,
}); });
/**
* No description
*
* @tags Query
* @name QueryMangaMangaConnectorIdDetail
* @summary Returns the API.Schema.MangaContext.MangaConnectorId`1 with API.Schema.MangaContext.MangaConnectorId`1.Key
* @request GET:/v2/Query/Manga/MangaConnectorId/{MangaConnectorIdId}
*/
queryMangaMangaConnectorIdDetail = (
mangaConnectorIdId: string,
params: RequestParams = {},
) =>
this.request<MangaMangaConnectorId, ProblemDetails>({
path: `/v2/Query/Manga/MangaConnectorId/${mangaConnectorIdId}`,
method: "GET",
format: "json",
...params,
});
/**
* No description
*
* @tags Query
* @name QueryChapterMangaConnectorIdDetail
* @summary Returns the API.Schema.MangaContext.MangaConnectorId`1 with API.Schema.MangaContext.MangaConnectorId`1.Key
* @request GET:/v2/Query/chapter/MangaConnectorId/{MangaConnectorIdId}
*/
queryChapterMangaConnectorIdDetail = (
mangaConnectorIdId: string,
params: RequestParams = {},
) =>
this.request<ChapterMangaConnectorId, ProblemDetails>({
path: `/v2/Query/chapter/MangaConnectorId/${mangaConnectorIdId}`,
method: "GET",
format: "json",
...params,
});
/** /**
* No description * No description
* *

View File

@@ -106,6 +106,32 @@ export interface Chapter {
key?: string | null; key?: string | null;
} }
export interface ChapterMangaConnectorId {
/**
* @minLength 0
* @maxLength 64
*/
objId: string;
/**
* @minLength 0
* @maxLength 32
*/
mangaConnectorName: string;
/**
* @minLength 0
* @maxLength 256
*/
idOnConnectorSite: string;
/**
* @format uri
* @minLength 0
* @maxLength 512
*/
websiteUrl?: string | null;
useForDownload?: boolean;
key?: string | null;
}
export interface FileLibrary { export interface FileLibrary {
/** /**
* @minLength 0 * @minLength 0
@@ -193,6 +219,7 @@ export interface Manga {
originalLanguage?: string | null; originalLanguage?: string | null;
chapterIds?: string[] | null; chapterIds?: string[] | null;
idsOnMangaConnectors?: Record<string, string>; idsOnMangaConnectors?: Record<string, string>;
mangaConnectorIdsIds?: string[] | null;
key?: string | null; key?: string | null;
} }
@@ -220,6 +247,32 @@ export interface MangaConnector {
enabled: boolean; enabled: boolean;
} }
export interface MangaMangaConnectorId {
/**
* @minLength 0
* @maxLength 64
*/
objId: string;
/**
* @minLength 0
* @maxLength 32
*/
mangaConnectorName: string;
/**
* @minLength 0
* @maxLength 256
*/
idOnConnectorSite: string;
/**
* @format uri
* @minLength 0
* @maxLength 512
*/
websiteUrl?: string | null;
useForDownload?: boolean;
key?: string | null;
}
export interface MangaTag { export interface MangaTag {
/** /**
* @minLength 0 * @minLength 0