Created standardized Popup-Window

Moved Update-functions for Queue-Status and Monitoring-list to their respective modules
This commit is contained in:
glax 2024-10-20 17:27:02 +02:00
parent 1304bc750a
commit fcc1ff392c
10 changed files with 166 additions and 135 deletions

View File

@ -1,10 +1,11 @@
import React, {EventHandler, useEffect} from 'react'; import React, {useEffect, useState} from 'react';
import Footer from "./modules/Footer"; import Footer from "./modules/Footer";
import Search from "./modules/Search"; import Search from "./modules/Search";
import Header from "./modules/Header"; import Header from "./modules/Header";
import MonitorJobsList from "./modules/MonitorJobsList"; import MonitorJobsList from "./modules/MonitorJobsList";
import './styles/index.css' import './styles/index.css'
import QueuePopUp from "./modules/QueuePopUp"; import {Job} from "./modules/Job";
import IFrontendSettings from "./modules/interfaces/IFrontendSettings";
export default function App(){ export default function App(){
const [connected, setConnected] = React.useState(false); const [connected, setConnected] = React.useState(false);
@ -15,35 +16,14 @@ export default function App(){
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number>(); const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number>();
useEffect(() => { useEffect(() => {
checkConnection(); checkConnection().then(res => setConnected(res)).catch(() => setConnected(false));
setInterval(() => { setInterval(() => {
checkConnection(); checkConnection().then(res => setConnected(res)).catch(() => setConnected(false));
}, 500); }, 500);
}, []); }, []);
const checkConnection = () =>{ function CreateJob(internalId: string, jobType: string){
getData('http://127.0.0.1:6531/v2/Ping').then((result) => { Job.CreateJobDateInterval(internalId, jobType, frontendSettings.jobInterval);
setConnected(result != null);
}).catch(() => setConnected(false));
}
useEffect(() => {
if(connected){
setLastJobListUpdate(new Date());
setJoblistUpdateInterval(setInterval(() => {
setLastJobListUpdate(new Date());
}, 5000));
}else{
clearInterval(joblistUpdateInterval);
setJoblistUpdateInterval(undefined);
}
}, [connected]);
const JobsChanged : EventHandler<any> = () => {
setLastMangaListUpdate(new Date());
setLastJobListUpdate(new Date());
} }
return(<div> return(<div>
@ -52,18 +32,14 @@ export default function App(){
? <> ? <>
{showSearch {showSearch
? <> ? <>
<Search onJobsChanged={JobsChanged} closeSearch={() => setShowSearch(false)} /> <Search createJob={CreateJob} closeSearch={() => setShowSearch(false)} />
<hr/> <hr/>
</> </>
: <></>} : <></>}
{showQueue <MonitorJobsList onStartSearch={() => setShowSearch(true)} onJobsChanged={() => console.info("jobsChanged")} connectedToBackend={connected} />
? <QueuePopUp closeQueue={() => setShowQueue(false)} />
: <></>
}
<MonitorJobsList onStartSearch={() => setShowSearch(true)} onJobsChanged={JobsChanged} key={lastMangaListUpdate.getTime()}/>
</> </>
: <h1>No connection to backend</h1>} : <h1>No connection to backend</h1>}
<Footer key={lastJobListUpdate.getTime()} showQueue={() => setShowQueue(true)}/> <Footer connectedToBackend={connected} />
</div>) </div>)
} }
@ -133,4 +109,10 @@ export function isValidUri(uri: string) : boolean{
} catch (err) { } catch (err) {
return false; return false;
} }
}
export const checkConnection = async (): Promise<boolean> =>{
return getData('http://127.0.0.1:6531/v2/Ping').then((result) => {
return result != null;
}).catch(() => Promise.reject());
} }

View File

@ -3,12 +3,14 @@ import '../styles/footer.css';
import {Job} from './Job'; import {Job} from './Job';
import Icon from '@mdi/react'; import Icon from '@mdi/react';
import { mdiRun, mdiCounter, mdiEyeCheck, mdiTrayFull } from '@mdi/js'; import { mdiRun, mdiCounter, mdiEyeCheck, mdiTrayFull } from '@mdi/js';
import QueuePopUp from "./QueuePopUp";
export default function Footer({showQueue} : {showQueue(): void}){ export default function Footer({connectedToBackend} : {connectedToBackend: boolean}) {
const [MonitoringJobsCount, setMonitoringJobsCount] = React.useState(0); const [MonitoringJobsCount, setMonitoringJobsCount] = React.useState(0);
const [AllJobsCount, setAllJobsCount] = React.useState(0); const [AllJobsCount, setAllJobsCount] = React.useState(0);
const [RunningJobsCount, setRunningJobsCount] = React.useState(0); const [RunningJobsCount, setRunningJobsCount] = React.useState(0);
const [StandbyJobsCount, setStandbyJobsCount] = React.useState(0); const [StandbyJobsCount, setStandbyJobsCount] = React.useState(0);
const [countUpdateInterval, setcountUpdateInterval] = React.useState<number>();
function UpdateBackendState(){ function UpdateBackendState(){
Job.GetMonitoringJobs().then((jobs) => setMonitoringJobsCount(jobs.length)); Job.GetMonitoringJobs().then((jobs) => setMonitoringJobsCount(jobs.length));
@ -18,15 +20,25 @@ export default function Footer({showQueue} : {showQueue(): void}){
} }
useEffect(() => { useEffect(() => {
UpdateBackendState(); if(connectedToBackend){
}, []); UpdateBackendState();
setcountUpdateInterval(setInterval(() => {
UpdateBackendState();
}, 2000));
}else{
clearInterval(countUpdateInterval);
setcountUpdateInterval(undefined);
}
}, [connectedToBackend]);
return ( return (
<footer> <footer>
<div><Icon path={mdiEyeCheck} size={1}/> <span>{MonitoringJobsCount}</span></div> <div className="statusBadge"><Icon path={mdiEyeCheck} size={1}/> <span>{MonitoringJobsCount}</span></div>
<div onClick={showQueue} className="hoverHand"><Icon path={mdiTrayFull} size={1}/> <span>{StandbyJobsCount}</span></div> <QueuePopUp>
<div onClick={showQueue} className="hoverHand"><Icon path={mdiRun} size={1}/> <span>{RunningJobsCount}</span></div> <div className="statusBadge hoverHand"><Icon path={mdiTrayFull} size={1}/> <span>{StandbyJobsCount}</span></div>
<div><Icon path={mdiCounter} size={1}/> <span>{AllJobsCount}</span></div> <div className="statusBadge hoverHand"><Icon path={mdiRun} size={1}/> <span>{RunningJobsCount}</span></div>
</QueuePopUp>
<div className="statusBadge"><Icon path={mdiCounter} size={1}/> <span>{AllJobsCount}</span></div>
<p id="madeWith">Made with Blåhaj 🦈</p> <p id="madeWith">Made with Blåhaj 🦈</p>
</footer>) </footer>)
} }

View File

@ -4,6 +4,10 @@ import IProgressToken from "./interfaces/IProgressToken";
export class Job export class Job
{ {
static IntervalStringFromDate(date: Date) : string {
return `${date.getDay()}.${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
}
static async GetAllJobs(): Promise<string[]> { static async GetAllJobs(): Promise<string[]> {
console.info("Getting all Jobs"); console.info("Getting all Jobs");
return getData("http://127.0.0.1:6531/v2/Jobs") return getData("http://127.0.0.1:6531/v2/Jobs")
@ -101,9 +105,18 @@ export class Job
}); });
} }
static async CreateJobDateInterval(internalId: string, jobType: string, interval: Date) : Promise<null> {
return this.CreateJob(internalId, jobType, this.IntervalStringFromDate(interval));
}
static async CreateJob(internalId: string, jobType: string, interval: string): Promise<null> { static async CreateJob(internalId: string, jobType: string, interval: string): Promise<null> {
const validate = /(?:[0-9]{1,2}\.)?[0-9]{1,2}:[0-9]{1,2}(?::[0-9]{1,2})?/
console.info(`Creating Job for Manga ${internalId} at ${interval} interval`); console.info(`Creating Job for Manga ${internalId} at ${interval} interval`);
let data = { if(!validate.test(interval)){
console.error("Interval was in incorrect format.");
return Promise.reject();
}
const data = {
internalId: internalId, internalId: internalId,
interval: interval interval: interval
}; };

View File

@ -8,9 +8,10 @@ import '../styles/MangaCoverCard.css'
import Icon from '@mdi/react'; import Icon from '@mdi/react';
import { mdiTrashCanOutline, mdiPlayBoxOutline } from '@mdi/js'; import { mdiTrashCanOutline, mdiPlayBoxOutline } from '@mdi/js';
export default function MonitorJobsList({onStartSearch, onJobsChanged} : {onStartSearch() : void, onJobsChanged: EventHandler<any>}) { export default function MonitorJobsList({onStartSearch, onJobsChanged, connectedToBackend} : {onStartSearch() : void, onJobsChanged: EventHandler<any>, connectedToBackend: boolean}) {
const [MonitoringJobs, setMonitoringJobs] = useState<IJob[]>([]); const [MonitoringJobs, setMonitoringJobs] = useState<IJob[]>([]);
const [AllManga, setAllManga] = useState<IManga[]>([]); const [AllManga, setAllManga] = useState<IManga[]>([]);
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number>();
useEffect(() => { useEffect(() => {
console.debug("Updating display list."); console.debug("Updating display list.");
@ -28,8 +29,16 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged} : {onStar
}, [MonitoringJobs]); }, [MonitoringJobs]);
useEffect(() => { useEffect(() => {
UpdateMonitoringJobsList(); if(connectedToBackend){
}, []); UpdateMonitoringJobsList();
setJoblistUpdateInterval(setInterval(() => {
UpdateMonitoringJobsList();
}, 1000));
}else{
clearInterval(joblistUpdateInterval);
setJoblistUpdateInterval(undefined);
}
}, [connectedToBackend]);
function UpdateMonitoringJobsList(){ function UpdateMonitoringJobsList(){
console.debug("Updating MonitoringJobsList"); console.debug("Updating MonitoringJobsList");

View File

@ -1,16 +1,18 @@
import React, {ReactElement, useEffect} from 'react'; import React, {useEffect, useState} from 'react';
import IJob, {JobTypeFromNumber} from "./interfaces/IJob"; import IJob, {JobTypeFromNumber} from "./interfaces/IJob";
import '../styles/queuePopUp.css'; import '../styles/queuePopUp.css';
import '../styles/popup.css';
import {Job} from "./Job"; import {Job} from "./Job";
import IManga from "./interfaces/IManga"; import IManga from "./interfaces/IManga";
import {Manga} from "./Manga"; import {Manga} from "./Manga";
export default function QueuePopUp({closeQueue} : {closeQueue(): void}){ export default function QueuePopUp({children} : {children: JSX.Element[]}) {
const [StandbyJobs, setStandbyJobs] = React.useState<IJob[]>([]); const [StandbyJobs, setStandbyJobs] = React.useState<IJob[]>([]);
const [StandbyJobsManga, setStandbyJobsManga] = React.useState<IManga[]>([]); const [StandbyJobsManga, setStandbyJobsManga] = React.useState<IManga[]>([]);
const [RunningJobs, setRunningJobs] = React.useState<IJob[]>([]); const [RunningJobs, setRunningJobs] = React.useState<IJob[]>([]);
const [RunningJobsManga, setRunningJobsManga] = React.useState<IManga[]>([]); const [RunningJobsManga, setRunningJobsManga] = React.useState<IManga[]>([]);
const [showQueuePopup, setShowQueuePopup] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
Job.GetStandbyJobs() Job.GetStandbyJobs()
@ -56,45 +58,54 @@ export default function QueuePopUp({closeQueue} : {closeQueue(): void}){
.then(setRunningJobsManga); .then(setRunningJobsManga);
}, [RunningJobs]); }, [RunningJobs]);
return ( return (<>
<div id="QueuePopUp"> <div onClick={() => setShowQueuePopup(true)}>
<div id="QueuePopUpHeader"> {children}
<h1>Queue Status</h1>
<img alt="Close Search" id="closeSearch" src="../media/close-x.svg" onClick={closeQueue}/>
</div> </div>
<div id="QueuePopUpBody"> {showQueuePopup
<div id="RunningJobQueue"> ? <div className="popup" id="QueuePopUp">
<h1>Running</h1> <div className="popupHeader">
<div className="JobQueue"> <h1>Queue Status {showQueuePopup ? "true" : "false"}</h1>
{RunningJobs.map((job: IJob) => { <img alt="Close Search" className="close" src="../media/close-x.svg"
const manga = RunningJobsManga.find(manga => manga.internalId == job.mangaInternalId || manga.internalId == job.chapter?.parentManga.internalId); onClick={() => setShowQueuePopup(false)}/>
if (manga === undefined || manga === null) </div>
return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga for {job.id}</div> <div id="QueuePopUpBody" className="popupBody">
return <div className="QueueJob" key={"QueueJob-" + job.id}> <div id="RunningJobQueue">
<img src={Manga.GetMangaCoverUrl(manga.internalId)} /> <h1>Running</h1>
<p>{JobTypeFromNumber(job.jobType)}</p> <div className="JobQueue">
</div>; {RunningJobs.map((job: IJob) => {
})} const manga = RunningJobsManga.find(manga => manga.internalId == job.mangaInternalId || manga.internalId == job.chapter?.parentManga.internalId);
if (manga === undefined || manga === null)
return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga
for {job.id}</div>
return <div className="QueueJob" key={"QueueJob-" + job.id}>
<img src={Manga.GetMangaCoverUrl(manga.internalId)}/>
<p>{JobTypeFromNumber(job.jobType)}</p>
</div>;
})}
</div>
</div>
<div id="WaitingJobQueue">
<h1>Standby</h1>
<div className="JobQueue">
{StandbyJobs.map((job: IJob) => {
const manga = StandbyJobsManga.find(manga => manga.internalId == job.mangaInternalId || manga.internalId == job.chapter?.parentManga.internalId);
if (manga === undefined || manga === null)
return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga
for {job.id}</div>
return <div className="QueueJob" key={"QueueJob-" + job.id}>
<img src={Manga.GetMangaCoverUrl(manga.internalId)}/>
<p className="QueueJob-Name">{manga.sortName}</p>
<p className="QueueJob-JobType">{JobTypeFromNumber(job.jobType)}</p>
<p className="QueueJob-additional">{job.jobType == 0 ? `Vol.${job.chapter?.volumeNumber} Ch.${job.chapter?.chapterNumber}` : ""}</p>
</div>;
})}
</div>
</div>
</div> </div>
</div> </div>
<div id="WaitingJobQueue"> : <></>
<h1>Standby</h1> }
<div className="JobQueue"> </>
{StandbyJobs.map((job: IJob) => {
const manga = StandbyJobsManga.find(manga => manga.internalId == job.mangaInternalId || manga.internalId == job.chapter?.parentManga.internalId);
if (manga === undefined || manga === null)
return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga
for {job.id}</div>
return <div className="QueueJob" key={"QueueJob-" + job.id}>
<img src={Manga.GetMangaCoverUrl(manga.internalId)}/>
<p className="QueueJob-Name">{manga.sortName}</p>
<p className="QueueJob-JobType">{JobTypeFromNumber(job.jobType)}</p>
<p className="QueueJob-additional">{job.jobType == 0 ? `Vol.${job.chapter?.volumeNumber} Ch.${job.chapter?.chapterNumber}` : ""}</p>
</div>;
})}
</div>
</div>
</div>
</div>
); );
} }

View File

@ -6,7 +6,7 @@ import IManga, {SearchResult} from "./interfaces/IManga";
import '../styles/search.css'; import '../styles/search.css';
import '../styles/MangaSearchResult.css' import '../styles/MangaSearchResult.css'
export default function Search({onJobsChanged, closeSearch} : {onJobsChanged: EventHandler<any>, closeSearch(): void}) { export default function Search({createJob, closeSearch} : {createJob: (internalId: string, type: string) => void, closeSearch(): void}) {
const [mangaConnectors, setConnectors] = useState<IMangaConnector[]>(); const [mangaConnectors, setConnectors] = useState<IMangaConnector[]>();
const [selectedConnector, setSelectedConnector] = useState<IMangaConnector>(); const [selectedConnector, setSelectedConnector] = useState<IMangaConnector>();
const [selectedLanguage, setSelectedLanguage] = useState<string>(); const [selectedLanguage, setSelectedLanguage] = useState<string>();
@ -98,8 +98,7 @@ export default function Search({onJobsChanged, closeSearch} : {onJobsChanged: Ev
<select id="Searchbox-language" onChange={changeSelectedLanguage} value={selectedLanguage === null ? "" : selectedLanguage}> <select id="Searchbox-language" onChange={changeSelectedLanguage} value={selectedLanguage === null ? "" : selectedLanguage}>
{selectedConnector === undefined {selectedConnector === undefined
? <option value="" disabled hidden>Select Connector</option> ? <option value="" disabled hidden>Select Connector</option>
: selectedConnector.SupportedLanguages.map(language => <option value={language} : selectedConnector.SupportedLanguages.map(language => <option value={language} key={language}>{language}</option>)}
key={language}>{language}</option>)}
</select> </select>
<button id="Searchbox-button" type="submit" onClick={ExecuteSearch}>Search</button> <button id="Searchbox-button" type="submit" onClick={ExecuteSearch}>Search</button>
</div> </div>
@ -107,7 +106,7 @@ export default function Search({onJobsChanged, closeSearch} : {onJobsChanged: Ev
<div id="SearchResults"> <div id="SearchResults">
{searchResults === undefined {searchResults === undefined
? <p></p> ? <p></p>
: searchResults.map(result => SearchResult(result, onJobsChanged))} : searchResults.map(result => SearchResult(result, createJob))}
</div> </div>
</div>) </div>)
} }

View File

@ -52,7 +52,7 @@ export function CoverCard(manga: IManga) : ReactElement {
</div>); </div>);
} }
export function SearchResult(manga: IManga, jobsChanged: EventHandler<any>) : ReactElement { export function SearchResult(manga: IManga, createJob: (internalId: string, type: string) => void) : ReactElement {
return( return(
<div className="SearchResult" key={manga.internalId}> <div className="SearchResult" key={manga.internalId}>
<img src={Manga.GetMangaCoverUrl(manga.internalId)}></img> <img src={Manga.GetMangaCoverUrl(manga.internalId)}></img>
@ -65,7 +65,7 @@ export function SearchResult(manga: IManga, jobsChanged: EventHandler<any>) : Re
</div> </div>
<MarkdownPreview className="Manga-description" source={manga.description} style={{ backgroundColor: "transparent", color: "black" }} /> <MarkdownPreview className="Manga-description" source={manga.description} style={{ backgroundColor: "transparent", color: "black" }} />
<button className="Manga-AddButton" onClick={(e) => { <button className="Manga-AddButton" onClick={(e) => {
Job.CreateJob(manga.internalId, "MonitorManga", "03:00:00").then(() => jobsChanged(manga.internalId)); createJob(manga.internalId, "MonitorManga")
}}>Monitor }}>Monitor
</button> </button>
</div>); </div>);

View File

@ -11,6 +11,7 @@ footer {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
color: white; color: white;
z-index: 10;
} }
#madeWith { #madeWith {
@ -20,19 +21,20 @@ footer {
cursor: url("Website/media/blahaj.png"), grab; cursor: url("Website/media/blahaj.png"), grab;
} }
footer div { footer .statusBadge {
margin: 0 10px; margin: 0 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-items: center; justify-items: center;
background-color: rgba(255,255,255, 0.3); background-color: rgba(255,255,255, 0.3);
border-radius: 10px; border-radius: 10px;
padding: 0 5px; padding: 2px 5px;
} }
footer div > * { footer > div {
margin: 0 2px; display: flex;
padding: 3px 0; align-items: center;
justify-items: center;
} }
footer .hoverHand { footer .hoverHand {

43
Website/styles/popup.css Normal file
View File

@ -0,0 +1,43 @@
.popup {
position: fixed;
left: 10%;
top: 7.5%;
width: 80%;
height: 80%;
margin: auto;
z-index: 100;
background-color: var(--second-background-color);
border-radius: 10px;
overflow: hidden;
}
.popup .popupHeader {
position: absolute;
top: 0;
left: 0;
height: 40px;
width: 100%;
background-color: var(--primary-color);
color: var(--accent-color);
}
.popup .popupHeader h1 {
margin: 4px 10px;
font-size: 20pt;
}
.popup .close {
position: absolute;
top: 0;
right: 0;
height: 100%;
cursor: pointer;
}
.popup .popupBody {
position: absolute;
top: 40px;
left: 0;
width: 100%;
height: calc(100% - 40px);
}

View File

@ -1,44 +1,4 @@
#QueuePopUp {
position: absolute;
left: 10%;
top: 7.5%;
width: 80%;
height: 80%;
margin: auto;
z-index: 100;
background-color: var(--second-background-color);
border-radius: 10px;
overflow: hidden;
}
#QueuePopUp #QueuePopUpHeader {
position: absolute;
top: 0;
left: 0;
height: 40px;
width: 100%;
background-color: var(--primary-color);
color: var(--accent-color);
}
#QueuePopUp #QueuePopUpHeader h1 {
margin: 4px 10px;
font-size: 20pt;
}
#QueuePopUp #closeSearch {
position: absolute;
top: 0;
right: 0;
height: 100%;
}
#QueuePopUp #QueuePopUpBody { #QueuePopUp #QueuePopUpBody {
position: absolute;
top: 40px;
left: 0;
width: 100%;
height: calc(100% - 40px);
display: flex; display: flex;
} }