Add Loader-Spinner

Style Settings, re-add api-url-field
This commit is contained in:
2025-03-19 02:37:36 +01:00
parent 0b0abb3801
commit 1d8dd7381d
26 changed files with 1041 additions and 629 deletions

View File

@ -7,18 +7,17 @@ export default interface IAuthor {
}
export function AuthorElement({apiUri, authorId} : {apiUri: string, authorId: string | null}) : ReactElement{
if(authorId === null)
return (<p className="Manga-Author-Name">Author</p>);
let [name, setName] = React.useState<string>(authorId);
let [author, setAuthor] = React.useState<IAuthor | null>(null);
useEffect(()=> {
if(authorId === null)
return;
getData(`${apiUri}/v2/Query/Author/${authorId}`)
.then((json) => {
let ret = json as IAuthor;
setName(ret.authorName);
setAuthor(ret);
});
}, [])
return (<p className="Manga-Author-Name">{name}</p>);
return (<span className="Manga-Author-Name">{author ? author.authorName : authorId}</span>);
}

View File

@ -8,20 +8,17 @@ export default interface ILink {
}
export function LinkElement({apiUri, linkId} : {apiUri: string, linkId: string | null}) : ReactElement{
if(linkId === null)
return (<a className="Manga-Link-Value" href="#">Link</a>);
let [provider, setProvider] = React.useState<string>(linkId);
let [linkUrl, setLinkUrl] = React.useState<string>("");
let [link, setLink] = React.useState<ILink | null>(null);
useEffect(()=> {
if(linkId === null)
return;
getData(`${apiUri}/v2/Query/Link/${linkId}`)
.then((json) => {
let ret = json as ILink;
setProvider(ret.linkProvider);
setLinkUrl(ret.linkUrl);
setLink(ret);
});
}, [])
return (<a className="Manga-Link-Value" href={linkUrl}>{provider}</a>);
return (<a className="Manga-Link-Value" href={link ? link.linkUrl : "#"}>{link ? link.linkProvider : linkId}</a>);
}

View File

@ -5,6 +5,8 @@ import { mdiTagTextOutline, mdiAccountEdit, mdiLinkVariant } from '@mdi/js';
import MarkdownPreview from '@uiw/react-markdown-preview';
import {AuthorElement} from "./IAuthor";
import {LinkElement} from "./ILink";
import IChapter from "./IChapter";
import Loader from "../Loader";
export default interface IManga{
mangaId: string;
@ -40,56 +42,87 @@ export function MangaItem({apiUri, mangaId, children} : {apiUri: string, mangaId
let [manga, setManga] = useState<IManga | null>(null);
let [clicked, setClicked] = useState<boolean>(false);
let [latestChapterDownloaded, setLatestChapterDownloaded] = useState<IChapter | null>(null);
let [latestChapterAvailable, setLatestChapterAvailable] = useState<IChapter | null>(null);
let [loadingChapterStats, setLoadingChapterStats] = useState<boolean>(true);
let [settingThreshold, setSettingThreshold] = useState<boolean>(false);
const invalidTargets = ["input", "textarea", "button", "select", "a"];
useEffect(() => {
MangaFunctions.GetMangaById(apiUri, mangaId).then(setManga);
MangaFunctions.GetLatestChapterDownloaded(apiUri, mangaId)
.then(setLatestChapterDownloaded)
.finally(() => {
if(latestChapterDownloaded && latestChapterAvailable)
setLoadingChapterStats(false);
});
MangaFunctions.GetLatestChapterAvailable(apiUri, mangaId)
.then(setLatestChapterAvailable)
.finally(() => {
if(latestChapterDownloaded && latestChapterAvailable)
setLoadingChapterStats(false);
});
}, []);
return (<div className="MangaItem" key={mangaId} is-clicked={clicked ? "clicked" : "not-clicked"} onClick={()=>setClicked(!clicked)}>
<img className="MangaItem-Cover" src={MangaFunctions.GetMangaCoverImageUrl(apiUri, mangaId, undefined)} alt="MangaFunctions Cover" onLoad={LoadMangaCover} onResize={LoadMangaCover}></img>
<p className="MangaItem-Connector">{manga ? manga.mangaConnectorId : "Connector"}</p>
<p className="MangaItem-Status" release-status={manga?.releaseStatus}></p>
<p className="MangaItem-Name">{manga ? manga.name : "Name"}</p>
return (<div className="MangaItem" key={mangaId} is-clicked={clicked ? "clicked" : "not-clicked"} onClick={(e)=> {
e.preventDefault();
const target = e.target as HTMLElement;
if(invalidTargets.find(x => x == target.localName) === undefined )
setClicked(!clicked)
}}>
<img className="MangaItem-Cover" src="../../media/blahaj.png" alt="MangaFunctions Cover" onLoad={LoadMangaCover} onResize={LoadMangaCover}></img>
<div className="MangaItem-Connector">{manga ? manga.mangaConnectorId : "Connector"}</div>
<div className="MangaItem-Status" release-status={manga?.releaseStatus}></div>
<div className="MangaItem-Name">{manga ? manga.name : "Name"}</div>
<a className="MangaItem-Website" href={manga ? manga.websiteUrl : "#"}><img src="../../media/link.svg" alt="Link"/></a>
<div className="MangaItem-Tags">
{manga ? manga.authorIds.map(authorId =>
<p className="MangaItem-Author" key={authorId} >
<div className="MangaItem-Author" key={authorId} >
<Icon path={mdiAccountEdit} size={0.5} />
<AuthorElement apiUri={apiUri} authorId={authorId}></AuthorElement>
</p>)
</div>)
:
<p className="MangaItem-Author">
<div className="MangaItem-Author" key="null-Author">
<Icon path={mdiAccountEdit} size={0.5} />
<AuthorElement apiUri={apiUri} authorId={null}></AuthorElement>
</p>}
</div>}
{manga ? manga.tags.map(tag =>
<p className="MangaItem-Tag" key={tag}>
<div className="MangaItem-Tag" key={tag}>
<Icon path={mdiTagTextOutline} size={0.5}/>
<p className="MangaItem-Tag-Value">{tag}</p>
</p>)
<span className="MangaItem-Tag-Value">{tag}</span>
</div>)
:
<p className="MangaItem-Tag">
<div className="MangaItem-Tag" key="null-Tag">
<Icon path={mdiTagTextOutline} size={0.5}/>
<p className="MangaItem-Tag-Value">Tag</p>
</p>
<span className="MangaItem-Tag-Value">Tag</span>
</div>
}
{manga ? manga.linkIds.map(linkId =>
<p className="MangaItem-Link" key={linkId}>
<div className="MangaItem-Link" key={linkId}>
<Icon path={mdiLinkVariant} size={0.5}/>
<LinkElement apiUri={apiUri} linkId={linkId} />
</p>)
</div>)
:
<p className="MangaItem-Link">
<div className="MangaItem-Link" key="null-Link">
<Icon path={mdiLinkVariant} size={0.5}/>
<LinkElement apiUri={apiUri} linkId={null} />
</p>}
</div>}
</div>
<MarkdownPreview className="MangaItem-Description" source={manga ? manga.description : "# Description"} />
<div className="MangaItem-Props">
<div className="MangaItem-Props-Threshold">
Start at Chapter
<input type="text" defaultValue={latestChapterDownloaded ? latestChapterDownloaded.chapterNumber : ""} disabled={settingThreshold} onChange={(e) => {
setSettingThreshold(true);
MangaFunctions.SetIgnoreThreshold(apiUri, mangaId, e.currentTarget.valueAsNumber).finally(()=>setSettingThreshold(false));
}} />
<Loader loading={settingThreshold} />
out of <span className="MangaItem-Props-Threshold-Available">{latestChapterAvailable ? latestChapterAvailable.chapterNumber : <Loader loading={loadingChapterStats}/>}</span>
</div>
{children ? children.map(c => {
if(c instanceof Element)
return c as ReactElement;
else
return <span>{c}</span>
return c
}) : null}
</div>
</div>)

View File

@ -1,5 +0,0 @@
export default interface IMangaAltTitle {
altTitleId: string;
language: string;
title: string;
}

View File

@ -0,0 +1,25 @@
import React, {ReactElement, useEffect} from "react";
import {getData} from "../../App";
import IAuthor from "./IAuthor";
export default interface IMangaAltTitle {
altTitleId: string;
language: string;
title: string;
}
export function AltTitleElement({apiUri, altTitleId} : {apiUri: string, altTitleId: string | null}) : ReactElement{
let [altTitle, setAltTitle] = React.useState<IMangaAltTitle | null>(null);
useEffect(()=> {
if(altTitleId === null)
return;
getData(`${apiUri}/v2/Query/AltTitle/${altTitleId}`)
.then((json) => {
let ret = json as IMangaAltTitle;
setAltTitle(ret);
});
}, [])
return (<span className="Manga-AltTitle">{altTitle ? altTitle.title : altTitleId}</span>);
}

View File

@ -1,4 +1,7 @@
import {ReactElement, ReactEventHandler, useState} from "react";
import "../../styles/notificationConnector.css";
import Loader from "../Loader";
import NotificationConnectorFunctions from "../NotificationConnectorFunctions";
export default interface INotificationConnector {
name: string;
@ -9,49 +12,77 @@ export default interface INotificationConnector {
}
export function NotificationConnectorItem({apiUri, notificationConnector} : {apiUri: string, notificationConnector: INotificationConnector | null}) : ReactElement {
const Save : ReactEventHandler<HTMLButtonElement> = (e) => {
}
const AddHeader : ReactEventHandler<HTMLButtonElement> = (e) => {
const div = e.currentTarget.parentElement as HTMLDivElement;
setHeaderElements([...headerElements, <HeaderElement record={null} />])
let header : Record<string, string> = {};
let x = info;
x.headers = [header, ...x.headers];
setInfo(x);
setHeaderElements([...headerElements, <HeaderElement record={header} />])
}
const [headerElements, setHeaderElements] = useState<ReactElement[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [info, setInfo] = useState<INotificationConnector>({
name: "",
url: "",
headers: [],
httpMethod: "",
body: ""
});
return (<div className="NotificationConnectorItem" key={notificationConnector ? notificationConnector.name : "new"}>
<p className="NotificationConnectorItem-Name">{notificationConnector ? notificationConnector.name : "New Notification Connector"}</p>
<select className="NotificationConnectorItem-RequestMethod" defaultValue={notificationConnector ? notificationConnector.httpMethod : ""} disabled={notificationConnector != null}>
<option value="" disabled hidden>Request Method</option>
<option value="GET">GET</option>
<option value="POST">POST</option>
</select>
<input type="url" className="NotificationConnectorItem-Url" placeholder="URL" value={notificationConnector ? notificationConnector.url : ""} disabled={notificationConnector != null} />
<textarea className="NotificationConnectorItem-Body" placeholder="Request-Body" value={notificationConnector ? notificationConnector.body : ""} disabled={notificationConnector != null} />
<div className="NotificationConnectorItem-Url">
<select className="NotificationConnectorItem-RequestMethod" defaultValue={notificationConnector ? notificationConnector.httpMethod : ""} disabled={notificationConnector != null} onChange={(e) => {
let x = info;
x.httpMethod = e.currentTarget.value;
setInfo(x);
}}>
<option value="" disabled hidden>Request Method</option>
<option value="GET">GET</option>
<option value="POST">POST</option>
</select>
<input type="url" className="NotificationConnectorItem-RequestUrl" placeholder="URL" defaultValue={notificationConnector ? notificationConnector.url : ""} disabled={notificationConnector != null} onChange={(e) => {
let x = info;
x.url = e.currentTarget.value;
setInfo(x);
}} />
</div>
<textarea className="NotificationConnectorItem-Body" placeholder="Request-Body" defaultValue={notificationConnector ? notificationConnector.body : ""} disabled={notificationConnector != null} onChange={(e) => {
let x = info;
x.body = e.currentTarget.value;
setInfo(x);
}} />
{notificationConnector != null ? null :
(
<p className="NotificationConnectorItem-Explanation">Explanation Text</p>
)}
<div className="NotificationConnectorItem-Headers">
{headerElements}
{notificationConnector
? notificationConnector.headers.map((h: Record<string, string>) =>
(<HeaderElement record={h} />)
(<HeaderElement record={h} disabled={notificationConnector != null}/>)
) :
(
<button className="NotificationConnectorItem-AddHeader" onClick={AddHeader}>Add Header</button>
)
}
{headerElements}
</div>
{notificationConnector != null ? null : (
<button className="NotificationConnectorItem-Save" onClick={Save}>Save</button>
<>
<button className="NotificationConnectorItem-Save" onClick={(e) => {
setLoading(true);
NotificationConnectorFunctions.CreateNotificationConnector(apiUri, info)
.finally(() => setLoading(false));
}}>Add</button>
<Loader loading={loading} style={{width:"40px",height:"40px",margin:"calc(sin(70)*(50% - 40px))"}}/>
</>
)}
</div>);
}
function HeaderElement({record} : {record: Record<string, string> | null}) : ReactElement {
return <div className="NotificationConnectorItem-Header" key={"newHeader"}>
<input type="text" className="NotificationConnectorItem-Header-Key" placeholder="Header-Key" value={record ? record.name : ""} disabled={record != null} />
<input type="text" className="NotificationConnectorItem-Header-Value" placeholder="Header-Value" value={record ? record.value : ""} disabled={record != null} />
function HeaderElement({record, disabled} : {record: Record<string, string>, disabled?: boolean | null}) : ReactElement {
return <div className="NotificationConnectorItem-Header" key={record.name}>
<input type="text" className="NotificationConnectorItem-Header-Key" placeholder="Header-Key" defaultValue={record.name} disabled={disabled?disabled:false} onChange={(e) => record.name = e.currentTarget.value}/>
<input type="text" className="NotificationConnectorItem-Header-Value" placeholder="Header-Value" defaultValue={record.value} disabled={disabled?disabled:false} onChange={(e) => record.value = e.currentTarget.value} />
</div>;
}