Make apiUri changeable from frontend
This commit is contained in:
parent
d2533ee98f
commit
d97eff9796
@ -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());
|
||||
}
|
@ -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>
|
||||
|
@ -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>)
|
||||
}
|
@ -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`, {});
|
||||
}
|
||||
}
|
@ -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`;
|
||||
}
|
||||
}
|
@ -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 (
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
18
Website/modules/interfaces/IBackendSettings.tsx
Normal file
18
Website/modules/interfaces/IBackendSettings.tsx
Normal 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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
13
Website/modules/interfaces/ILibraryConnector.tsx
Normal file
13
Website/modules/interfaces/ILibraryConnector.tsx
Normal 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 "";
|
||||
}
|
8
Website/modules/interfaces/INotificationConnector.tsx
Normal file
8
Website/modules/interfaces/INotificationConnector.tsx
Normal 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
65
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user