Make apiUri changeable from frontend

This commit is contained in:
glax 2024-10-20 20:12:27 +02:00
parent d2533ee98f
commit d97eff9796
14 changed files with 339 additions and 108 deletions

View File

@ -5,26 +5,42 @@ import Header from "./modules/Header";
import MonitorJobsList from "./modules/MonitorJobsList";
import './styles/index.css'
import {Job} from "./modules/Job";
import IFrontendSettings from "./modules/interfaces/IFrontendSettings";
import IFrontendSettings, {FrontendSettingsWith} from "./modules/interfaces/IFrontendSettings";
import {useCookies} from "react-cookie";
export default function App(){
const [, setCookie] = useCookies(['apiUri', 'jobInterval']);
const [connected, setConnected] = React.useState(false);
const [showSearch, setShowSearch] = React.useState(false);
const [frontendSettings, setFrontendSettings] = useState<IFrontendSettings>({jobInterval: new Date(0,0,0,3)});
const [frontendSettings, setFrontendSettings] = useState<IFrontendSettings>(FrontendSettingsWith(undefined, undefined, undefined));
const [updateInterval, setUpdateInterval] = React.useState<number>();
const apiUri = frontendSettings.apiUri;
useEffect(() => {
checkConnection().then(res => setConnected(res)).catch(() => setConnected(false));
setInterval(() => {
checkConnection().then(res => setConnected(res)).catch(() => setConnected(false));
}, 500);
}, []);
checkConnection(apiUri).then(res => setConnected(res)).catch(() => setConnected(false));
if(updateInterval === undefined){
setUpdateInterval(setInterval(() => {
checkConnection(apiUri).then(res => setConnected(res)).catch(() => setConnected(false));
}, 500));
}else{
clearInterval(updateInterval);
setUpdateInterval(undefined);
}
}, [frontendSettings]);
function ChangeSettings(settings: IFrontendSettings) {
setFrontendSettings(settings);
setCookie('apiUri', settings.apiUri);
setCookie('jobInterval', settings.jobInterval);
}
function CreateJob(internalId: string, jobType: string){
Job.CreateJobDateInterval(internalId, jobType, frontendSettings.jobInterval);
Job.CreateJobDateInterval(apiUri, internalId, jobType, frontendSettings.jobInterval);
}
return(<div>
<Header settings={frontendSettings} changeSettings={setFrontendSettings}/>
<Header apiUri={apiUri} backendConnected={connected} settings={frontendSettings} changeSettings={ChangeSettings}/>
{connected
? <>
{showSearch
@ -33,10 +49,10 @@ export default function App(){
<hr/>
</>
: <></>}
<MonitorJobsList onStartSearch={() => setShowSearch(true)} onJobsChanged={() => console.info("jobsChanged")} connectedToBackend={connected} />
<MonitorJobsList apiUri={apiUri} onStartSearch={() => setShowSearch(true)} onJobsChanged={() => console.info("jobsChanged")} connectedToBackend={connected} />
</>
: <h1>No connection to backend</h1>}
<Footer connectedToBackend={connected} />
<Footer apiUri={apiUri} connectedToBackend={connected} />
</div>)
}
@ -108,8 +124,8 @@ export function isValidUri(uri: string) : boolean{
}
}
export const checkConnection = async (): Promise<boolean> =>{
return getData('http://127.0.0.1:6531/v2/Ping').then((result) => {
export const checkConnection = async (apiUri: string): Promise<boolean> =>{
return getData(`${apiUri}/v2/Ping`).then((result) => {
return result != null;
}).catch(() => Promise.reject());
}

View File

@ -5,7 +5,7 @@ import Icon from '@mdi/react';
import { mdiRun, mdiCounter, mdiEyeCheck, mdiTrayFull } from '@mdi/js';
import QueuePopUp from "./QueuePopUp";
export default function Footer({connectedToBackend} : {connectedToBackend: boolean}) {
export default function Footer({connectedToBackend, apiUri} : {connectedToBackend: boolean, apiUri: string}) {
const [MonitoringJobsCount, setMonitoringJobsCount] = React.useState(0);
const [AllJobsCount, setAllJobsCount] = React.useState(0);
const [RunningJobsCount, setRunningJobsCount] = React.useState(0);
@ -13,10 +13,10 @@ export default function Footer({connectedToBackend} : {connectedToBackend: boole
const [countUpdateInterval, setcountUpdateInterval] = React.useState<number>();
function UpdateBackendState(){
Job.GetMonitoringJobs().then((jobs) => setMonitoringJobsCount(jobs.length));
Job.GetAllJobs().then((jobs) => setAllJobsCount(jobs.length));
Job.GetRunningJobs().then((jobs) => setRunningJobsCount(jobs.length));
Job.GetStandbyJobs().then((jobs) => setStandbyJobsCount(jobs.length));
Job.GetMonitoringJobs(apiUri).then((jobs) => setMonitoringJobsCount(jobs.length));
Job.GetAllJobs(apiUri).then((jobs) => setAllJobsCount(jobs.length));
Job.GetRunningJobs(apiUri).then((jobs) => setRunningJobsCount(jobs.length));
Job.GetStandbyJobs(apiUri).then((jobs) => setStandbyJobsCount(jobs.length));
}
useEffect(() => {
@ -34,7 +34,7 @@ export default function Footer({connectedToBackend} : {connectedToBackend: boole
return (
<footer>
<div className="statusBadge"><Icon path={mdiEyeCheck} size={1}/> <span>{MonitoringJobsCount}</span></div>
<QueuePopUp>
<QueuePopUp apiUri={apiUri}>
<div className="statusBadge hoverHand"><Icon path={mdiTrayFull} size={1}/> <span>{StandbyJobsCount}</span></div>
<div className="statusBadge hoverHand"><Icon path={mdiRun} size={1}/> <span>{RunningJobsCount}</span></div>
</QueuePopUp>

View File

@ -3,13 +3,13 @@ import '../styles/header.css'
import Settings from "./Settings";
import IFrontendSettings from "./interfaces/IFrontendSettings";
export default function Header({settings, changeSettings} : {settings: IFrontendSettings, changeSettings(settings: IFrontendSettings): void}){
export default function Header({backendConnected, apiUri, settings, changeSettings} : {backendConnected: boolean, apiUri: string, settings: IFrontendSettings, changeSettings(settings: IFrontendSettings): void}){
return (
<header>
<div id="titlebox">
<img alt="website image is Blahaj" src="../media/blahaj.png"/>
<span>Tranga</span>
</div>
<Settings settings={settings} changeSettings={changeSettings} />
<Settings settings={settings} changeSettings={changeSettings} backendConnected={backendConnected} apiUri={apiUri}/>
</header>)
}

View File

@ -8,108 +8,108 @@ export class Job
return `${date.getDay()}.${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
}
static async GetAllJobs(): Promise<string[]> {
static async GetAllJobs(apiUri: string): Promise<string[]> {
console.info("Getting all Jobs");
return getData("http://127.0.0.1:6531/v2/Jobs")
return getData(`${apiUri}/v2/Jobs`)
.then((json) => {
console.info("Got all Jobs");
const ret = json as string[];
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static async GetRunningJobs(): Promise<string[]> {
static async GetRunningJobs(apiUri: string): Promise<string[]> {
console.info("Getting all running Jobs");
return getData("http://127.0.0.1:6531/v2/Jobs/Running")
return getData(`${apiUri}/v2/Jobs/Running`)
.then((json) => {
console.info("Got all running Jobs");
const ret = json as string[];
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static async GetWaitingJobs(): Promise<string[]> {
static async GetWaitingJobs(apiUri: string): Promise<string[]> {
console.info("Getting all waiting Jobs");
return getData("http://127.0.0.1:6531/v2/Jobs/Waiting")
return getData(`${apiUri}/v2/Jobs/Waiting`)
.then((json) => {
console.info("Got all waiting Jobs");
const ret = json as string[];
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static async GetStandbyJobs(): Promise<string[]> {
static async GetStandbyJobs(apiUri: string): Promise<string[]> {
console.info("Getting all standby Jobs");
return getData("http://127.0.0.1:6531/v2/Jobs/Standby")
return getData(`${apiUri}/v2/Jobs/Standby`)
.then((json) => {
console.info("Got all standby Jobs");
const ret = json as string[];
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static async GetMonitoringJobs(): Promise<string[]> {
static async GetMonitoringJobs(apiUri: string): Promise<string[]> {
console.info("Getting all monitoring Jobs");
return getData("http://127.0.0.1:6531/v2/Jobs/Monitoring")
return getData(`${apiUri}/v2/Jobs/Monitoring`)
.then((json) => {
console.info("Got all monitoring Jobs");
const ret = json as string[];
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static async GetJob(jobId: string): Promise<IJob>{
static async GetJob(apiUri: string, jobId: string): Promise<IJob>{
if(jobId === undefined || jobId === null || jobId.length < 1) {
console.error(`JobId was not provided`);
return Promise.reject();
}
console.info(`Getting Job ${jobId}`);
return getData(`http://127.0.0.1:6531/v2/Job/${jobId}`)
return getData(`${apiUri}/v2/Job/${jobId}`)
.then((json) => {
console.info(`Got Job ${jobId}`);
const ret = json as IJob;
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static async GetJobs(jobIds: string[]): Promise<IJob[]> {
static async GetJobs(apiUri: string, jobIds: string[]): Promise<IJob[]> {
if(jobIds === undefined || jobIds === null || jobIds.length < 1) {
console.error(`JobIds was not provided`);
return Promise.reject();
}
let reqStr = jobIds.join(",");
console.info(`Getting Jobs ${reqStr}`);
return getData(`http://127.0.0.1:6531/v2/Job?jobIds=${reqStr}`)
return getData(`${apiUri}/v2/Job?jobIds=${reqStr}`)
.then((json) => {
console.info(`Got Jobs ${reqStr}`);
const ret = json as IJob[];
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static async GetJobProgress(jobId: string): Promise<IProgressToken> {
static async GetJobProgress(apiUri: string, jobId: string): Promise<IProgressToken> {
console.info(`Getting Job ${jobId} Progress`);
return getData(`http://127.0.0.1:6531/v2/Job/${jobId}/Progress`)
return getData(`${apiUri}/v2/Job/${jobId}/Progress`)
.then((json) => {
console.info(`Got Job ${jobId} Progress`);
const ret = json as IProgressToken;
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static async CreateJobDateInterval(internalId: string, jobType: string, interval: Date) : Promise<null> {
return this.CreateJob(internalId, jobType, this.IntervalStringFromDate(interval));
static async CreateJobDateInterval(apiUri: string, internalId: string, jobType: string, interval: Date) : Promise<null> {
return this.CreateJob(apiUri, internalId, jobType, this.IntervalStringFromDate(interval));
}
static async CreateJob(internalId: string, jobType: string, interval: string): Promise<null> {
static async CreateJob(apiUri: string, 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`);
if(!validate.test(interval)){
@ -120,22 +120,22 @@ export class Job
internalId: internalId,
interval: interval
};
return postData(`http://127.0.0.1:6531/v2/Job/Create/${jobType}`, data)
return postData(`${apiUri}/v2/Job/Create/${jobType}`, data)
.then((json) => {
console.info(`Created Job for Manga ${internalId} at ${interval} interval`);
return null;
});
}
static DeleteJob(jobId: string) : Promise<void> {
return deleteData(`http://127.0.0.1:6531/v2/Job/${jobId}`);
static DeleteJob(apiUri: string, jobId: string) : Promise<void> {
return deleteData(`${apiUri}/v2/Job/${jobId}`);
}
static StartJob(jobId: string) : Promise<object> {
return postData(`http://127.0.0.1:6531/v2/Job/${jobId}/StartNow`, {});
static StartJob(apiUri: string, jobId: string) : Promise<object> {
return postData(`${apiUri}/v2/Job/${jobId}/StartNow`, {});
}
static CancelJob(jobId: string) : Promise<object> {
return postData(`http://127.0.0.1:6531/v2/Job/${jobId}/Cancel`, {});
static CancelJob(apiUri: string, jobId: string) : Promise<object> {
return postData(`${apiUri}/v2/Job/${jobId}/Cancel`, {});
}
}

View File

@ -3,52 +3,52 @@ import { getData } from '../App';
export class Manga
{
static async GetAllManga(): Promise<IManga[]> {
static async GetAllManga(apiUri: string): Promise<IManga[]> {
console.info("Getting all Manga");
return getData("http://127.0.0.1:6531/v2/Mangas")
return getData(`${apiUri}/v2/Mangas`)
.then((json) => {
console.info("Got all Manga");
const ret = json as IManga[];
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static async SearchManga(name: string): Promise<IManga[]> {
static async SearchManga(apiUri: string, name: string): Promise<IManga[]> {
console.info(`Getting Manga ${name} from all Connectors`);
return await getData(`http://127.0.0.1:6531/v2/Manga/Search?title=${name}`)
return await getData(`${apiUri}/v2/Manga/Search?title=${name}`)
.then((json) => {
console.info(`Got Manga ${name}`);
const ret = json as IManga[];
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static async GetMangaById(internalId: string): Promise<IManga> {
static async GetMangaById(apiUri: string, internalId: string): Promise<IManga> {
console.info(`Getting Manga ${internalId}`);
return await getData(`http://127.0.0.1:6531/v2/Manga/${internalId}`)
return await getData(`${apiUri}/v2/Manga/${internalId}`)
.then((json) => {
console.info(`Got Manga ${internalId}`);
const ret = json as IManga;
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static async GetMangaByIds(internalIds: string[]): Promise<IManga[]> {
static async GetMangaByIds(apiUri: string, internalIds: string[]): Promise<IManga[]> {
console.debug(`Getting Mangas ${internalIds.join(",")}`);
return await getData(`http://127.0.0.1:6531/v2/Manga?mangaIds=${internalIds.join(",")}`)
return await getData(`${apiUri}/v2/Manga?mangaIds=${internalIds.join(",")}`)
.then((json) => {
console.debug(`Got Manga ${internalIds.join(",")}`);
const ret = json as IManga[];
console.debug(ret);
//console.debug(ret);
return (ret);
});
}
static GetMangaCoverUrl(internalId: string): string {
static GetMangaCoverUrl(apiUri: string, internalId: string): string {
console.debug(`Getting Manga Cover-Url ${internalId}`);
return `http://127.0.0.1:6531/v2/Manga/${internalId}/Cover`;
return `${apiUri}/v2/Manga/${internalId}/Cover`;
}
}

View File

@ -8,7 +8,7 @@ import '../styles/MangaCoverCard.css'
import Icon from '@mdi/react';
import { mdiTrashCanOutline, mdiPlayBoxOutline } from '@mdi/js';
export default function MonitorJobsList({onStartSearch, onJobsChanged, connectedToBackend} : {onStartSearch() : void, onJobsChanged: EventHandler<any>, connectedToBackend: boolean}) {
export default function MonitorJobsList({onStartSearch, onJobsChanged, connectedToBackend, apiUri} : {onStartSearch() : void, onJobsChanged: EventHandler<any>, connectedToBackend: boolean, apiUri: string}) {
const [MonitoringJobs, setMonitoringJobs] = useState<IJob[]>([]);
const [AllManga, setAllManga] = useState<IManga[]>([]);
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number>();
@ -23,7 +23,7 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged, connected
MonitoringJobs.forEach(job => {
if(AllManga.find(manga => manga.internalId == job.mangaInternalId))
return;
Manga.GetMangaById(job.mangaInternalId != undefined ? job.mangaInternalId : job.chapter != undefined ? job.chapter.parentManga.internalId : "").
Manga.GetMangaById(apiUri, job.mangaInternalId != undefined ? job.mangaInternalId : job.chapter != undefined ? job.chapter.parentManga.internalId : "").
then((manga: IManga) => setAllManga([...AllManga, manga]));
});
}, [MonitoringJobs]);
@ -42,10 +42,10 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged, connected
function UpdateMonitoringJobsList(){
console.debug("Updating MonitoringJobsList");
Job.GetMonitoringJobs()
Job.GetMonitoringJobs(apiUri)
.then((jobs) => {
if(jobs.length > 0)
return Job.GetJobs(jobs)
return Job.GetJobs(apiUri, jobs)
return [];
})
.then((jobs) => setMonitoringJobs(jobs));
@ -66,13 +66,13 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged, connected
const DeleteJob : MouseEventHandler = (e) => {
const jobId = e.currentTarget.id.slice(e.currentTarget.id.indexOf("-")+1);
console.info(`Pressed ${e.currentTarget.id} => ${jobId}`);
Job.DeleteJob(jobId).then(() => onJobsChanged(jobId));
Job.DeleteJob(apiUri, jobId).then(() => onJobsChanged(jobId));
}
const StartJob : MouseEventHandler = (e) => {
const jobId = e.currentTarget.id.slice(e.currentTarget.id.indexOf("-")+1);
console.info(`Pressed ${e.currentTarget.id} => ${jobId}`);
Job.StartJob(jobId);
Job.StartJob(apiUri, jobId);
}
return (

View File

@ -6,7 +6,7 @@ import {Job} from "./Job";
import IManga from "./interfaces/IManga";
import {Manga} from "./Manga";
export default function QueuePopUp({children} : {children: JSX.Element[]}) {
export default function QueuePopUp({children, apiUri} : {children: JSX.Element[], apiUri: string}) {
const [StandbyJobs, setStandbyJobs] = React.useState<IJob[]>([]);
const [StandbyJobsManga, setStandbyJobsManga] = React.useState<IManga[]>([]);
@ -15,10 +15,10 @@ export default function QueuePopUp({children} : {children: JSX.Element[]}) {
const [showQueuePopup, setShowQueuePopup] = useState<boolean>(false);
useEffect(() => {
Job.GetStandbyJobs()
Job.GetStandbyJobs(apiUri)
.then((jobs) => {
if(jobs.length > 0)
return Job.GetJobs(jobs);
return Job.GetJobs(apiUri, jobs);
return [];
})
.then((jobs) => {
@ -27,10 +27,10 @@ export default function QueuePopUp({children} : {children: JSX.Element[]}) {
setStandbyJobs(jobs.filter(job => job.jobType <= 1));
console.log(StandbyJobs)
});
Job.GetRunningJobs()
Job.GetRunningJobs(apiUri)
.then((jobs) => {
if(jobs.length > 0)
return Job.GetJobs(jobs);
return Job.GetJobs(apiUri, jobs);
return [];
})
.then((jobs) =>{
@ -44,7 +44,7 @@ export default function QueuePopUp({children} : {children: JSX.Element[]}) {
return;
const mangaIds = StandbyJobs.filter(job => job.jobType<=2).map((job) => job.mangaInternalId != undefined ? job.mangaInternalId : job.chapter != undefined ? job.chapter.parentManga.internalId : "");
console.debug(`Waiting mangaIds: ${mangaIds.join(",")}`);
Manga.GetMangaByIds(mangaIds)
Manga.GetMangaByIds(apiUri, mangaIds)
.then(setStandbyJobsManga);
}, [StandbyJobs]);
@ -54,7 +54,7 @@ export default function QueuePopUp({children} : {children: JSX.Element[]}) {
console.log(RunningJobs);
const mangaIds = RunningJobs.filter(job => job.jobType<=2).map((job) => job.mangaInternalId != undefined ? job.mangaInternalId : job.chapter != undefined ? job.chapter.parentManga.internalId : "");
console.debug(`Running mangaIds: ${mangaIds.join(",")}`);
Manga.GetMangaByIds(mangaIds)
Manga.GetMangaByIds(apiUri, mangaIds)
.then(setRunningJobsManga);
}, [RunningJobs]);
@ -79,7 +79,7 @@ export default function QueuePopUp({children} : {children: JSX.Element[]}) {
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)}/>
<img src={Manga.GetMangaCoverUrl(apiUri, manga.internalId)}/>
<p>{JobTypeFromNumber(job.jobType)}</p>
</div>;
})}
@ -94,7 +94,7 @@ export default function QueuePopUp({children} : {children: JSX.Element[]}) {
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)}/>
<img src={Manga.GetMangaCoverUrl(apiUri, 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>

View File

@ -1,10 +1,86 @@
import React, {useState} from 'react';
import IFrontendSettings from "./interfaces/IFrontendSettings";
import React, {KeyboardEventHandler, useEffect, useState} from 'react';
import IFrontendSettings, {FrontendSettingsWith} from "./interfaces/IFrontendSettings";
import '../styles/settings.css';
import IBackendSettings from "./interfaces/IBackendSettings";
import {getData} from "../App";
import ILibraryConnector from "./interfaces/ILibraryConnector";
import INotificationConnector from "./interfaces/INotificationConnector";
export default function Settings({settings, changeSettings} : {settings: IFrontendSettings, changeSettings(settings: IFrontendSettings): void}) {
export default function Settings({backendConnected, apiUri, settings, changeSettings} : {backendConnected: boolean, apiUri: string, settings: IFrontendSettings, changeSettings: (settings: IFrontendSettings) => void}) {
const [frontendSettings] = useState<IFrontendSettings>(settings);
const [backendSettings, setBackendSettings] = useState<IBackendSettings>();
const [showSettings, setShowSettings] = useState<boolean>(false);
const [libraryConnectors, setLibraryConnectors] = useState<ILibraryConnector[]>([]);
const [notificationConnectors, setNotificationConnectors] = useState<INotificationConnector[]>([]);
useEffect(() => {
if(!showSettings || !backendConnected)
return;
GetSettings(apiUri).then(setBackendSettings).catch(console.error);
GetLibraryConnectors(apiUri).then(setLibraryConnectors).catch(console.error);
GetNotificationConnectors(apiUri).then(setNotificationConnectors).catch(console.error);
}, [showSettings]);
async function GetSettings(apiUri: string) : Promise<IBackendSettings> {
console.info("Getting Settings");
return getData(`${apiUri}/v2/Settings`)
.then((json) => {
console.info("Got Settings");
const ret = json as IBackendSettings;
//console.debug(ret);
return (ret);
})
.catch(Promise.reject);
}
async function GetLibraryConnectors(apiUri: string) : Promise<ILibraryConnector[]> {
console.info("Getting Library Connectors");
return getData(`${apiUri}/v2/LibraryConnector`)
.then((json) => {
console.info("Got Library Connectors");
const ret = json as ILibraryConnector[];
//console.debug(ret);
return (ret);
})
.catch(Promise.reject);
}
async function GetNotificationConnectors(apiUri: string) : Promise<INotificationConnector[]> {
console.info("Getting Notification Connectors");
return getData(`${apiUri}/v2/NotificationConnector`)
.then((json) => {
console.info("Got Notification Connectors");
const ret = json as INotificationConnector[];
//console.debug(ret);
return (ret);
})
.catch(Promise.reject);
}
function GetKomga() : ILibraryConnector | undefined {
return libraryConnectors.find(con => con.libraryType == 0);
}
function GetKavita() : ILibraryConnector | undefined {
return libraryConnectors.find(con => con.libraryType == 1);
}
function GetGotify() : INotificationConnector | undefined {
return notificationConnectors.find(con => con.notificationConnectorType == 0);
}
function GetLunasea() : INotificationConnector | undefined {
return notificationConnectors.find(con => con.notificationConnectorType == 1);
}
function GetNtfy() : INotificationConnector | undefined {
return notificationConnectors.find(con => con.notificationConnectorType == 2);
}
const SubmitApiUri : KeyboardEventHandler<HTMLInputElement> = (e) => {
if(e.key == "Enter")
changeSettings(settings);
}
return (
<div id="Settings">
@ -22,20 +98,20 @@ export default function Settings({settings, changeSettings} : {settings: IFronte
<div className="section-item">
<span className="settings-section-title">API Settings</span>
<label htmlFor="settingApiUri">API URI:</label>
<input placeholder="https://" type="text" id="settingApiUri" />
<input placeholder={frontendSettings.apiUri} type="text" id="settingApiUri" onKeyDown={SubmitApiUri} />
<label htmlFor="userAgent">User Agent:</label>
<input placeholder="UserAgent" id="userAgent" type="text" />
<input id="userAgent" type="text" placeholder={backendSettings != undefined ? backendSettings.userAgent : "UserAgent"} />
</div>
<div className="section-item">
<span className="settings-section-title">Rate Limits</span>
<label htmlFor="DefaultRL">Default:</label>
<input id="defaultRL" type="text" />
<input id="defaultRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.Default.toString() : "-1"} />
<label htmlFor="CoverRL">Manga Covers:</label>
<input id="coverRL" type="text" />
<input id="coverRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaCover.toString() : "-1"} />
<label htmlFor="ImageRL">Manga Images:</label>
<input id="imageRL" type="text" />
<input id="imageRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaImage.toString() : "-1"} />
<label htmlFor="InfoRL">Manga Info:</label>
<input id="infoRL" type="text" />
<input id="infoRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaInfo.toString() : "-1"} />
</div>
<div className="section-item">
<span className="settings-section-title">Appearance</span>
@ -57,9 +133,9 @@ export default function Settings({settings, changeSettings} : {settings: IFronte
<a href="https://mangadex.org">MangaDex</a>
</span>
<label htmlFor="mDexFeedRL">Feed Rate Limit:</label>
<input id="mDexFeedRL" type="text" />
<input id="mDexFeedRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaDexFeed.toString() : "-1"} />
<label htmlFor="mDexImageRL">Image Rate Limit:</label>
<input id="mDexImageRL" type="text" />
<input id="mDexImageRL" type="number" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaDexImage.toString() : "-1"} />
</div>
</div>
</div>
@ -73,11 +149,11 @@ export default function Settings({settings, changeSettings} : {settings: IFronte
Komga
</span>
<label htmlFor="komgaUrl"></label>
<input placeholder="URL" id="komgaUrl" type="text" />
<input placeholder={GetKomga() != undefined ? GetKomga()?.baseUrl : "URL"} id="komgaUrl" type="text" />
<label htmlFor="komgaUsername"></label>
<input placeholder="Username" id="komgaUsername" type="text" />
<input placeholder={GetKomga() != undefined ? "***" : "Username"} id="komgaUsername" type="text" />
<label htmlFor="komgaPassword"></label>
<input placeholder="Password" id="komgaPassword" type="password" />
<input placeholder={GetKomga() != undefined ? "***" : "Password"} id="komgaPassword" type="password" />
<div className="section-buttons-container">
<span>Test</span>
<span>Reset</span>
@ -90,11 +166,11 @@ export default function Settings({settings, changeSettings} : {settings: IFronte
Kavita
</span>
<label htmlFor="kavitaUrl"></label>
<input placeholder="URL" id="kavitaUrl" type="text" />
<input placeholder={GetKavita() != undefined ? GetKavita()?.baseUrl : "URL"} id="kavitaUrl" type="text" />
<label htmlFor="kavitaUsername"></label>
<input placeholder="Username" id="kavitaUsername" type="text" />
<input placeholder={GetKavita() != undefined ? "***" : "Username"} id="kavitaUsername" type="text" />
<label htmlFor="kavitaPassword"></label>
<input placeholder="Password" id="kavitaPassword" type="password"/>
<input placeholder={GetKavita() != undefined ? "***" : "Password"} id="kavitaPassword" type="password"/>
<div className="section-buttons-container">
<span>Test</span>
<span>Reset</span>
@ -113,9 +189,9 @@ export default function Settings({settings, changeSettings} : {settings: IFronte
Gotify
</span>
<label htmlFor="gotifyUrl"></label>
<input placeholder="URL" id="gotifyUrl" type="text" />
<input placeholder={GetGotify() != undefined ? GetGotify()?.endpoint : "URL"} id="gotifyUrl" type="text" />
<label htmlFor="gotifyAppToken"></label>
<input placeholder="App-Token" id="gotifyAppToken" type="text" />
<input placeholder={GetGotify() != undefined ? GetGotify()?.appToken : "AppToken"} id="gotifyAppToken" type="text" />
<div className="section-buttons-container">
<span>Test</span>
<span>Reset</span>
@ -128,7 +204,7 @@ export default function Settings({settings, changeSettings} : {settings: IFronte
LunaSea
</span>
<label htmlFor="lunaseaWebhook"></label>
<input placeholder="device/:id or user/:id" id="lunaseaWebhook" type="text"/>
<input placeholder={GetLunasea() != undefined ? GetLunasea()?.id : "device/:id or user/:id"} id="lunaseaWebhook" type="text"/>
<div className="section-buttons-container">
<span>Test</span>
<span>Reset</span>
@ -141,9 +217,13 @@ export default function Settings({settings, changeSettings} : {settings: IFronte
Ntfy
</span>
<label htmlFor="ntfyEndpoint"></label>
<input placeholder="URL" id="ntfyEndpoint" type="text" />
<label htmlFor="ntfyAuth"></label>
<input placeholder="Auth" id="ntfyAuth" type="text" />
<input placeholder={GetNtfy() != undefined ? GetNtfy()?.endpoint : "URL"} id="ntfyEndpoint" type="text"/>
<label htmlFor="ntfyUsername">Username</label>
<input placeholder={GetNtfy() != undefined ? "***" : "Username"} id="ntfyUsername" type="text"/>
<label htmlFor="ntfyPassword">Password</label>
<input placeholder={GetNtfy() != undefined ? "***" : "Password"} id="ntfyPassword" type="password"/>
<label htmlFor="ntfyTopic">Topic</label>
<input placeholder={GetNtfy() != undefined ? GetNtfy()?.topic : "Topic"} id="ntfyTopic" type="text"/>
<div className="section-buttons-container">
<span>Test</span>
<span>Reset</span>

View File

@ -0,0 +1,18 @@
export default interface IBackendSettings {
"downloadLocation": string;
"workingDirectory": string;
"apiPortNumber": number;
"userAgent": string;
"bufferLibraryUpdates": boolean;
"bufferNotifications": boolean;
"version": number;
"aprilFoolsMode": boolean;
"requestLimits": {
"MangaInfo": number;
"MangaDexFeed": number;
"MangaDexImage": number;
"MangaImage": number;
"MangaCover": number;
"Default": number
}
}

View File

@ -1,3 +1,33 @@
import {Cookies} from "react-cookie";
export default interface IFrontendSettings {
jobInterval: Date;
apiUri: string;
}
export function FrontendSettingsWith(settings: IFrontendSettings | undefined, jobInterval: Date | undefined, apiUri: string | undefined): IFrontendSettings {
const cookies = new Cookies();
let transform : IFrontendSettings;
if(settings === undefined) {
transform = {
apiUri: apiUri === undefined
? cookies.get('apiUri') === undefined
? `${window.location.protocol}//${window.location.host}/api`
: cookies.get('apiUri')
: apiUri,
jobInterval: jobInterval === undefined
? cookies.get('jobInterval') === undefined
? new Date(0,0,0,3)
: cookies.get('jobInterval')
: jobInterval
}
}else {
transform = {
apiUri: apiUri === undefined ? settings.apiUri : apiUri,
jobInterval: jobInterval === undefined ? settings.jobInterval : jobInterval,
}
}
console.debug(settings);
console.debug(transform);
return transform;
}

View File

@ -0,0 +1,13 @@
export default interface ILibraryConnector {
libraryType: number;
baseUrl: string;
auth: string;
}
export function GetLibraryConnectorNameFromNumber(n: number): string {
switch(n){
case 0: return "Komga";
case 1: return "Kavita";
}
return "";
}

View File

@ -0,0 +1,8 @@
export default interface INotificationConnector {
"notificationConnectorType": number; //see NotificationConnectorType
"endpoint": string | undefined;//only on Ntfy, Gotify
"appToken": string | undefined;//only on Gotify
"auth": string | undefined;//only on Ntfy
"topic": string | undefined;//only on Ntfy
"id": string | undefined;//only on LunaSea
}

65
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@mdi/react": "^1.6.1",
"@types/react": "^18.2.0",
"react": "^18.3.1",
"react-cookie": "^7.2.1",
"react-dom": "^18.3.1",
"typescript": "^5.6.3",
"vite": "^5.4.9"
@ -661,6 +662,13 @@
"win32"
]
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@ -694,6 +702,17 @@
"@types/unist": "*"
}
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
"integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@ -873,6 +892,16 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/css-selector-parser": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.0.5.tgz",
@ -1351,6 +1380,16 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@ -2464,6 +2503,21 @@
"node": ">=0.10.0"
}
},
"node_modules/react-cookie": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.2.1.tgz",
"integrity": "sha512-UcE6njAy5UmKzU9RywIvvtbHIQydhh6JP55yVbGwQ8Tp/wSPp2HOYVZdRI55zfbkW6zQv9SEs42mS3a9cIUQ0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.5",
"hoist-non-react-statics": "^3.3.2",
"universal-cookie": "^7.0.0"
},
"peerDependencies": {
"react": ">= 16.3.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@ -2977,6 +3031,17 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/universal-cookie": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.2.1.tgz",
"integrity": "sha512-GEKneQ0sz8qbobkYM5s9elAx6l7GQDNVl3Siqmc7bt/YccyyXWDPn+fht3J1qMcaLwPrzkty3i+dNfPGP2/9hA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^0.7.2"
}
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",

View File

@ -4,6 +4,7 @@
"@mdi/react": "^1.6.1",
"@types/react": "^18.2.0",
"react": "^18.3.1",
"react-cookie": "^7.2.1",
"react-dom": "^18.3.1",
"typescript": "^5.6.3",
"vite": "^5.4.9"