mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-04-12 14:38:21 +02:00
Adjust all endpoints and methods to tranga/postgres-Server-V2.
Search working.
This commit is contained in:
parent
0402f9e6d0
commit
84520d8e18
@ -14,6 +14,7 @@ export default function App(){
|
||||
const [frontendSettings, setFrontendSettings] = useState<IFrontendSettings>(LoadFrontendSettings());
|
||||
const [updateInterval, setUpdateInterval] = React.useState<number>();
|
||||
const [updateMonitorList, setUpdateMonitorList] = React.useState<Date>(new Date());
|
||||
const checkConnectedInterval = 1000;
|
||||
|
||||
const apiUri = frontendSettings.apiUri;
|
||||
|
||||
@ -22,7 +23,7 @@ export default function App(){
|
||||
if(updateInterval === undefined){
|
||||
setUpdateInterval(setInterval(() => {
|
||||
checkConnection(apiUri).then(res => setConnected(res)).catch(() => setConnected(false));
|
||||
}, 500));
|
||||
}, checkConnectedInterval));
|
||||
}else{
|
||||
clearInterval(updateInterval);
|
||||
setUpdateInterval(undefined);
|
||||
@ -76,7 +77,7 @@ export function getData(uri: string) : Promise<object> {
|
||||
});
|
||||
}
|
||||
|
||||
export function postData(uri: string, content: object) : Promise<object> {
|
||||
export function postData(uri: string, content: object | string | number) : Promise<object> {
|
||||
return fetch(uri,
|
||||
{
|
||||
method: 'POST',
|
||||
@ -116,6 +117,50 @@ export function deleteData(uri: string) : Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export function patchData(uri: string, content: object | string | number) : Promise<object> {
|
||||
return fetch(uri,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers : {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(content)
|
||||
})
|
||||
.then(function(response){
|
||||
if(!response.ok)
|
||||
throw new Error("Could not fetch data");
|
||||
let json = response.json();
|
||||
return json.then((json) => json).catch(() => null);
|
||||
})
|
||||
.catch(function(err){
|
||||
console.error(`Error PATCHing Data ${uri}\n${err}`);
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
|
||||
export function putData(uri: string, content: object | string | number) : Promise<object> {
|
||||
return fetch(uri,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers : {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(content)
|
||||
})
|
||||
.then(function(response){
|
||||
if(!response.ok)
|
||||
throw new Error("Could not fetch data");
|
||||
let json = response.json();
|
||||
return json.then((json) => json).catch(() => null);
|
||||
})
|
||||
.catch(function(err){
|
||||
console.error(`Error PUTting Data ${uri}\n${err}`);
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
|
||||
export function isValidUri(uri: string) : boolean{
|
||||
try {
|
||||
new URL(uri);
|
||||
@ -126,7 +171,14 @@ export function isValidUri(uri: string) : boolean{
|
||||
}
|
||||
|
||||
export const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
||||
return getData(`${apiUri}/v2/Ping`).then((result) => {
|
||||
return result != null;
|
||||
}).catch(() => Promise.reject());
|
||||
return fetch(`${apiUri}/swagger`,
|
||||
{
|
||||
method: 'GET',
|
||||
})
|
||||
.then((response) =>{
|
||||
return response.type != "error";
|
||||
})
|
||||
.catch(() => {
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
@ -2,21 +2,22 @@ import React, {useEffect} from 'react';
|
||||
import '../styles/footer.css';
|
||||
import Job from './Job';
|
||||
import Icon from '@mdi/react';
|
||||
import { mdiRun, mdiCounter, mdiEyeCheck, mdiTrayFull } from '@mdi/js';
|
||||
import {mdiCounter, mdiEyeCheck, mdiRun, mdiTrayFull} from '@mdi/js';
|
||||
import QueuePopUp from "./QueuePopUp";
|
||||
import {JobState, JobType} from "./interfaces/IJob";
|
||||
|
||||
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);
|
||||
const [StandbyJobsCount, setStandbyJobsCount] = React.useState(0);
|
||||
const [WaitingJobsCount, setWaitingJobs] = React.useState(0);
|
||||
const [countUpdateInterval, setCountUpdateInterval] = React.useState<number>();
|
||||
|
||||
function UpdateBackendState(){
|
||||
Job.GetMonitoringJobs(apiUri).then((jobs) => setMonitoringJobsCount(jobs.length));
|
||||
Job.GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob).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));
|
||||
Job.GetJobsInState(apiUri, JobState.Running).then((jobs) => setRunningJobsCount(jobs.length));
|
||||
Job.GetJobsInState(apiUri, JobState.Waiting).then((jobs) => setWaitingJobs(jobs.length));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -39,7 +40,7 @@ export default function Footer({connectedToBackend, apiUri} : {connectedToBacken
|
||||
<div className="statusBadge hoverHand"><Icon path={mdiRun} size={1}/> <span>{RunningJobsCount}</span>
|
||||
</div>
|
||||
<span>+</span>
|
||||
<div className="statusBadge hoverHand"><Icon path={mdiTrayFull} size={1}/><span>{StandbyJobsCount}</span></div>
|
||||
<div className="statusBadge hoverHand"><Icon path={mdiTrayFull} size={1}/><span>{WaitingJobsCount}</span></div>
|
||||
</QueuePopUp>
|
||||
<span>=</span>
|
||||
<div className="statusBadge"><Icon path={mdiCounter} size={1}/> <span>{AllJobsCount}</span></div>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import '../styles/header.css'
|
||||
import Settings from "./Settings";
|
||||
import IFrontendSettings from "./interfaces/IFrontendSettings";
|
||||
|
||||
export default function Header({backendConnected, apiUri, settings, changeSettings} : {backendConnected: boolean, apiUri: string, settings: IFrontendSettings, changeSettings(settings: IFrontendSettings): void}){
|
||||
@ -10,6 +9,5 @@ export default function Header({backendConnected, apiUri, settings, changeSettin
|
||||
<img alt="website image is Blahaj" src="../media/blahaj.png"/>
|
||||
<span>Tranga</span>
|
||||
</div>
|
||||
<Settings settings={settings} changeSettings={changeSettings} backendConnected={backendConnected} apiUri={apiUri}/>
|
||||
</header>)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import {deleteData, getData, postData} from '../App';
|
||||
import IJob from "./interfaces/IJob";
|
||||
import IProgressToken from "./interfaces/IProgressToken";
|
||||
import {deleteData, getData, patchData, postData, putData} from '../App';
|
||||
import IJob, {JobState, JobType} from "./interfaces/IJob";
|
||||
import IModifyJobRecord from "./interfaces/records/IModifyJobRecord";
|
||||
|
||||
export default class Job
|
||||
{
|
||||
@ -9,56 +9,68 @@ export default class Job
|
||||
return `${x.getDay()}.${x.getHours()}:${x.getMinutes()}:${x.getSeconds()}`;
|
||||
}
|
||||
|
||||
static async GetAllJobs(apiUri: string): Promise<string[]> {
|
||||
static async GetAllJobs(apiUri: string): Promise<IJob[]> {
|
||||
//console.info("Getting all Jobs");
|
||||
return getData(`${apiUri}/v2/Jobs`)
|
||||
return getData(`${apiUri}/v2/Job`)
|
||||
.then((json) => {
|
||||
//console.info("Got all Jobs");
|
||||
const ret = json as string[];
|
||||
const ret = json as IJob[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetRunningJobs(apiUri: string): Promise<string[]> {
|
||||
//console.info("Getting all running Jobs");
|
||||
return getData(`${apiUri}/v2/Jobs/Running`)
|
||||
static async GetJobsWithIds(apiUri: string, jobIds: string[]): Promise<IJob[]> {
|
||||
return postData(`${apiUri}/v2/Job/WithIDs`, jobIds)
|
||||
.then((json) => {
|
||||
//console.info("Got all running Jobs");
|
||||
const ret = json as string[];
|
||||
//console.info("Got all Jobs");
|
||||
const ret = json as IJob[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetJobsInState(apiUri: string, state: JobState): Promise<IJob[]> {
|
||||
if(state == null || state == undefined) {
|
||||
console.error(`state was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return getData(`${apiUri}/v2/Job/State/${state}`)
|
||||
.then((json) => {
|
||||
//console.info("Got all Jobs");
|
||||
const ret = json as IJob[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetWaitingJobs(apiUri: string): Promise<string[]> {
|
||||
//console.info("Getting all waiting Jobs");
|
||||
return getData(`${apiUri}/v2/Jobs/Waiting`)
|
||||
static async GetJobsWithType(apiUri: string, jobType: JobType): Promise<IJob[]> {
|
||||
if(jobType == null || jobType == undefined) {
|
||||
console.error(`jobType was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return getData(`${apiUri}/v2/Job/Type/${jobType}`)
|
||||
.then((json) => {
|
||||
//console.info("Got all waiting Jobs");
|
||||
const ret = json as string[];
|
||||
//console.info("Got all Jobs");
|
||||
const ret = json as IJob[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetStandbyJobs(apiUri: string): Promise<string[]> {
|
||||
//console.info("Getting all standby Jobs");
|
||||
return getData(`${apiUri}/v2/Jobs/Standby`)
|
||||
static async GetJobsOfTypeAndWithState(apiUri: string, jobType: JobType, state: JobState): Promise<IJob[]> {
|
||||
if(jobType == null || jobType == undefined) {
|
||||
console.error(`jobType was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
if(state == null || state == undefined) {
|
||||
console.error(`state was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return getData(`${apiUri}/v2/Job/TypeAndState/${jobType}/${state}`)
|
||||
.then((json) => {
|
||||
//console.info("Got all standby Jobs");
|
||||
const ret = json as string[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetMonitoringJobs(apiUri: string): Promise<string[]> {
|
||||
//console.info("Getting all monitoring Jobs");
|
||||
return getData(`${apiUri}/v2/Jobs/Monitoring`)
|
||||
.then((json) => {
|
||||
//console.info("Got all monitoring Jobs");
|
||||
const ret = json as string[];
|
||||
//console.info("Got all Jobs");
|
||||
const ret = json as IJob[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
@ -79,64 +91,117 @@ export default class Job
|
||||
});
|
||||
}
|
||||
|
||||
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(`${apiUri}/v2/Job?jobIds=${reqStr}`)
|
||||
.then((json) => {
|
||||
//console.info(`Got Jobs ${reqStr}`);
|
||||
const ret = json as IJob[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetJobProgress(apiUri: string, jobId: string): Promise<IProgressToken> {
|
||||
//console.info(`Getting 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);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
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(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)){
|
||||
console.error("Interval was in incorrect format.");
|
||||
return Promise.reject();
|
||||
}
|
||||
const data = {
|
||||
internalId: internalId,
|
||||
interval: interval
|
||||
};
|
||||
return postData(`${apiUri}/v2/Job/Create/${jobType}`, data)
|
||||
.then((json) => {
|
||||
//console.info(`Created Job for Manga ${internalId} at ${interval} interval`);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
static DeleteJob(apiUri: string, jobId: string) : Promise<void> {
|
||||
if(jobId === undefined || jobId === null || jobId.length < 1) {
|
||||
console.error(`JobId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return deleteData(`${apiUri}/v2/Job/${jobId}`);
|
||||
}
|
||||
|
||||
static StartJob(apiUri: string, jobId: string) : Promise<object> {
|
||||
return postData(`${apiUri}/v2/Job/${jobId}/StartNow`, {});
|
||||
static async ModifyJob(apiUri: string, jobId: string, modifyData: IModifyJobRecord): Promise<IJob> {
|
||||
if(jobId === undefined || jobId === null || jobId.length < 1) {
|
||||
console.error(`JobId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
if(modifyData === undefined || modifyData === null) {
|
||||
console.error(`modifyData was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return patchData(`${apiUri}/v2/Job/${jobId}`, modifyData)
|
||||
.then((json) => {
|
||||
//console.info(`Got Job ${jobId}`);
|
||||
const ret = json as IJob;
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static CancelJob(apiUri: string, jobId: string) : Promise<object> {
|
||||
return postData(`${apiUri}/v2/Job/${jobId}/Cancel`, {});
|
||||
static async CreateDownloadAvailableChaptersJob(apiUri: string, mangaId: string, recurrenceMs: number): Promise<string[]> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
if(recurrenceMs === undefined || recurrenceMs === null || recurrenceMs < 0) {
|
||||
console.error(`recurrenceMs was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return putData(`${apiUri}/v2/Job/DownloadAvailableChaptersJob/${mangaId}`, recurrenceMs)
|
||||
.then((json) => {
|
||||
//console.info(`Got Job ${jobId}`);
|
||||
const ret = json as string[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async CreateDownloadSingleChapterJob(apiUri: string, chapterId: string): Promise<string[]> {
|
||||
if(chapterId === undefined || chapterId === null || chapterId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return putData(`${apiUri}/v2/Job/DownloadSingleChapterJob/${chapterId}`, {})
|
||||
.then((json) => {
|
||||
//console.info(`Got Job ${jobId}`);
|
||||
const ret = json as string[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async CreateUpdateFilesJob(apiUri: string, mangaId: string): Promise<string[]> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return putData(`${apiUri}/v2/Job/UpdateFilesJob/${mangaId}`, {})
|
||||
.then((json) => {
|
||||
//console.info(`Got Job ${jobId}`);
|
||||
const ret = json as string[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async CreateUpdateAllFilesJob(apiUri: string): Promise<string[]> {
|
||||
return putData(`${apiUri}/v2/Job/UpdateAllFilesJob`, {})
|
||||
.then((json) => {
|
||||
//console.info(`Got Job ${jobId}`);
|
||||
const ret = json as string[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async CreateUpdateMetadataJob(apiUri: string, mangaId: string): Promise<string[]> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return putData(`${apiUri}/v2/Job/UpdateMetadataJob/${mangaId}`, {})
|
||||
.then((json) => {
|
||||
//console.info(`Got Job ${jobId}`);
|
||||
const ret = json as string[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async CreateUpdateAllMetadataJob(apiUri: string): Promise<string[]> {
|
||||
return putData(`${apiUri}/v2/Job/UpdateAllMetadataJob`, {})
|
||||
.then((json) => {
|
||||
//console.info(`Got Job ${jobId}`);
|
||||
const ret = json as string[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static StartJob(apiUri: string, jobId: string) : Promise<object> {
|
||||
return postData(`${apiUri}/v2/Job/${jobId}/Start`, {});
|
||||
}
|
||||
|
||||
static StopJob(apiUri: string, jobId: string) : Promise<object> {
|
||||
return postData(`${apiUri}/v2/Job/${jobId}/Stop`, {});
|
||||
}
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import IManga from './interfaces/IManga';
|
||||
import { getData } from '../App';
|
||||
import {deleteData, getData, patchData, postData} from '../App';
|
||||
import {RefObject} from "react";
|
||||
import IChapter from "./interfaces/IChapter";
|
||||
|
||||
export default class Manga
|
||||
{
|
||||
static async GetAllManga(apiUri: string): Promise<IManga[]> {
|
||||
//console.info("Getting all Manga");
|
||||
return getData(`${apiUri}/v2/Mangas`)
|
||||
return getData(`${apiUri}/v2/Manga`)
|
||||
.then((json) => {
|
||||
//console.info("Got all Manga");
|
||||
const ret = json as IManga[];
|
||||
@ -14,31 +16,13 @@ export default class Manga
|
||||
});
|
||||
}
|
||||
|
||||
static async SearchManga(apiUri: string, name: string): Promise<IManga[]> {
|
||||
//console.info(`Getting Manga ${name} from all Connectors`);
|
||||
return await getData(`${apiUri}/v2/Manga/Search?title=${name}`)
|
||||
.then((json) => {
|
||||
//console.info(`Got Manga ${name}`);
|
||||
const ret = json as IManga[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetMangaById(apiUri: string, internalId: string): Promise<IManga> {
|
||||
//console.info(`Getting Manga ${internalId}`);
|
||||
return await getData(`${apiUri}/v2/Manga/${internalId}`)
|
||||
.then((json) => {
|
||||
//console.info(`Got Manga ${internalId}`);
|
||||
const ret = json as IManga;
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetMangaByIds(apiUri: string, internalIds: string[]): Promise<IManga[]> {
|
||||
static async GetMangaWithIds(apiUri: string, mangaIds: string[]): Promise<IManga[]> {
|
||||
if(mangaIds === undefined || mangaIds === null || mangaIds.length < 1) {
|
||||
console.error(`mangaIds was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
//console.debug(`Getting Mangas ${internalIds.join(",")}`);
|
||||
return await getData(`${apiUri}/v2/Manga?mangaIds=${internalIds.join(",")}`)
|
||||
return await postData(`${apiUri}/v2/Manga/WithIds`, mangaIds)
|
||||
.then((json) => {
|
||||
//console.debug(`Got Manga ${internalIds.join(",")}`);
|
||||
const ret = json as IManga[];
|
||||
@ -47,8 +31,127 @@ export default class Manga
|
||||
});
|
||||
}
|
||||
|
||||
static GetMangaCoverUrl(apiUri: string, internalId: string, ref: HTMLElement): string {
|
||||
static async GetMangaById(apiUri: string, mangaId: string): Promise<IManga> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
//console.info(`Getting Manga ${internalId}`);
|
||||
return await getData(`${apiUri}/v2/Manga/${mangaId}`)
|
||||
.then((json) => {
|
||||
//console.info(`Got Manga ${internalId}`);
|
||||
const ret = json as IManga;
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async DeleteManga(apiUri: string, mangaId: string): Promise<void> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return deleteData(`${apiUri}/v2/Manga/${mangaId}`);
|
||||
}
|
||||
|
||||
static GetMangaCoverImageUrl(apiUri: string, mangaId: string, ref: HTMLImageElement | undefined): string {
|
||||
//console.debug(`Getting Manga Cover-Url ${internalId}`);
|
||||
return `${apiUri}/v2/Manga/${internalId}/Cover?dimensions=${ref.clientWidth*1.5}x${ref.clientHeight*1.5}`;
|
||||
if(ref == null || ref == undefined)
|
||||
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=64&height=64`;
|
||||
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`;
|
||||
}
|
||||
|
||||
static async GetChapters(apiUri: string, mangaId: string): Promise<IChapter[]> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return getData(`${apiUri}/v2/Manga/${mangaId}/Chapters`)
|
||||
.then((json) => {
|
||||
//console.info(`Got Manga ${internalId}`);
|
||||
const ret = json as IChapter[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetDownloadedChapters(apiUri: string, mangaId: string): Promise<IChapter[]> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/Downloaded`)
|
||||
.then((json) => {
|
||||
//console.info(`Got Manga ${internalId}`);
|
||||
const ret = json as IChapter[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetNotDownloadedChapters(apiUri: string, mangaId: string): Promise<IChapter[]> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/NotDownloaded`)
|
||||
.then((json) => {
|
||||
//console.info(`Got Manga ${internalId}`);
|
||||
const ret = json as IChapter[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetLatestChapterAvailable(apiUri: string, mangaId: string): Promise<IChapter> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestAvailable`)
|
||||
.then((json) => {
|
||||
//console.info(`Got Manga ${internalId}`);
|
||||
const ret = json as IChapter;
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetLatestChapterDownloaded(apiUri: string, mangaId: string): Promise<IChapter> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestDownloaded`)
|
||||
.then((json) => {
|
||||
//console.info(`Got Manga ${internalId}`);
|
||||
const ret = json as IChapter;
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async SetIgnoreThreshold(apiUri: string, mangaId: string, chapterThreshold: number): Promise<object> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
if(chapterThreshold === undefined || chapterThreshold === null) {
|
||||
console.error(`chapterThreshold was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return patchData(`${apiUri}/v2/Manga/${mangaId}/IgnoreChaptersBefore`, {chapterThreshold});
|
||||
}
|
||||
|
||||
static async MoveFolder(apiUri: string, mangaId: string, newPath: string): Promise<object> {
|
||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
||||
console.error(`mangaId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
if(newPath === undefined || newPath === null || newPath.length < 1) {
|
||||
console.error(`newPath was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return postData(`${apiUri}/v2/Manga/{MangaId}/MoveFolder`, {newPath});
|
||||
}
|
||||
}
|
@ -1,33 +1,44 @@
|
||||
import IMangaConnector from './interfaces/IMangaConnector';
|
||||
import IManga from './interfaces/IManga';
|
||||
import { getData } from '../App';
|
||||
import {getData, patchData} from '../App';
|
||||
|
||||
export class MangaConnector
|
||||
{
|
||||
static async GetAllConnectors(): Promise<IMangaConnector[]> {
|
||||
static async GetAllConnectors(apiUri: string): Promise<IMangaConnector[]> {
|
||||
//console.info("Getting all MangaConnectors");
|
||||
return getData("http://127.0.0.1:6531/v2/Connector/Types")
|
||||
return getData(`${apiUri}/v2/MangaConnector`)
|
||||
.then((json) => {
|
||||
//console.info("Got all MangaConnectors");
|
||||
return (json as IMangaConnector[]);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetMangaFromConnectorByTitle(connector: IMangaConnector, name: string): Promise<IManga[]> {
|
||||
//console.info(`Getting Manga ${name}`);
|
||||
return await getData(`http://127.0.0.1:6531/v2/Connector/${connector.name}/GetManga?title=${name}`)
|
||||
static async GetEnabledConnectors(apiUri: string): Promise<IMangaConnector[]> {
|
||||
//console.info("Getting all enabled MangaConnectors");
|
||||
return getData(`${apiUri}/v2/MangaConnector/enabled`)
|
||||
.then((json) => {
|
||||
//console.info(`Got Manga ${name}`);
|
||||
return (json as IManga[]);
|
||||
//console.info("Got all enabled MangaConnectors");
|
||||
return (json as IMangaConnector[]);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetMangaFromConnectorByUrl(connector: IMangaConnector, url: string): Promise<IManga> {
|
||||
//console.info(`Getting Manga ${url}`);
|
||||
return await getData(`http://127.0.0.1:6531/v2/Connector/${connector.name}/GetManga?url=${url}`)
|
||||
static async GetDisabledConnectors(apiUri: string): Promise<IMangaConnector[]> {
|
||||
//console.info("Getting all disabled MangaConnectors");
|
||||
return getData(`${apiUri}/v2/MangaConnector/disabled`)
|
||||
.then((json) => {
|
||||
//console.info(`Got Manga ${url}`);
|
||||
return (json as IManga);
|
||||
//console.info("Got all disabled MangaConnectors");
|
||||
return (json as IMangaConnector[]);
|
||||
});
|
||||
}
|
||||
|
||||
static async SetConnectorEnabled(apiUri: string, connectorName: string, enabled: boolean): Promise<object> {
|
||||
if(connectorName === undefined || connectorName === null || connectorName.length < 1) {
|
||||
console.error(`connectorName was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
if(enabled === undefined || enabled === null) {
|
||||
console.error(`enabled was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return patchData(`${apiUri}/v2/MangaConnector/${connectorName}/SetEnabled/${enabled}`, {});
|
||||
}
|
||||
}
|
@ -1,12 +1,9 @@
|
||||
import React, {EventHandler, MouseEventHandler, ReactElement, useEffect, useState} from 'react';
|
||||
import Job from './Job';
|
||||
import '../styles/monitorMangaList.css';
|
||||
import IJob from "./interfaces/IJob";
|
||||
import IManga, {CoverCard} from "./interfaces/IManga";
|
||||
import Manga from './Manga';
|
||||
import IJob, {JobType} from "./interfaces/IJob";
|
||||
import IManga from "./interfaces/IManga";
|
||||
import '../styles/MangaCoverCard.css'
|
||||
import Icon from '@mdi/react';
|
||||
import { mdiTrashCanOutline, mdiPlayBoxOutline } from '@mdi/js';
|
||||
|
||||
export default function MonitorJobsList({onStartSearch, onJobsChanged, connectedToBackend, apiUri, updateList} : {onStartSearch() : void, onJobsChanged: EventHandler<any>, connectedToBackend: boolean, apiUri: string, updateList: Date}) {
|
||||
const [MonitoringJobs, setMonitoringJobs] = useState<IJob[]>([]);
|
||||
@ -14,18 +11,7 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged, connected
|
||||
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number>();
|
||||
|
||||
useEffect(() => {
|
||||
//console.debug("Updating display list.");
|
||||
//Remove all Manga that are not associated with a Job
|
||||
setAllManga(AllManga.filter(manga => MonitoringJobs.find(job => job.mangaInternalId == manga.internalId)));
|
||||
//Fetch Manga that are missing (from Jobs)
|
||||
if(MonitoringJobs === null)
|
||||
return;
|
||||
MonitoringJobs.forEach(job => {
|
||||
if(AllManga.find(manga => manga.internalId == job.mangaInternalId))
|
||||
return;
|
||||
Manga.GetMangaById(apiUri, job.mangaInternalId != undefined ? job.mangaInternalId : job.chapter != undefined ? job.chapter.parentManga.internalId : "").
|
||||
then((manga: IManga) => setAllManga([...AllManga, manga]));
|
||||
});
|
||||
|
||||
}, [MonitoringJobs]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -48,12 +34,7 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged, connected
|
||||
if(!connectedToBackend)
|
||||
return;
|
||||
//console.debug("Updating MonitoringJobsList");
|
||||
Job.GetMonitoringJobs(apiUri)
|
||||
.then((jobs) => {
|
||||
if(jobs.length > 0)
|
||||
return Job.GetJobs(apiUri, jobs)
|
||||
return [];
|
||||
})
|
||||
Job.GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob)
|
||||
.then((jobs) => setMonitoringJobs(jobs));
|
||||
}
|
||||
|
||||
@ -84,17 +65,5 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged, connected
|
||||
return (
|
||||
<div id="MonitorMangaList">
|
||||
{StartSearchMangaEntry()}
|
||||
{AllManga.map((manga: IManga) => {
|
||||
const job = MonitoringJobs.find(job => job.mangaInternalId == manga.internalId);
|
||||
if (job === undefined || job == null)
|
||||
return <div>Error. Could not find matching job for {manga.internalId}</div>
|
||||
return <div key={"monitorMangaEntry-" + manga.internalId} className="monitorMangaEntry">
|
||||
{CoverCard(apiUri, manga)}
|
||||
<div className="MangaActionButtons">
|
||||
<div id={"Delete-"+job.id} className="DeleteJobButton" onClick={DeleteJob}><Icon path={mdiTrashCanOutline} size={1.5} /></div>
|
||||
<div id={"Start-"+job.id} className="StartJobNowButton" onClick={StartJob}><Icon path={mdiPlayBoxOutline} size={1.5} /></div>
|
||||
</div>
|
||||
</div>;
|
||||
})}
|
||||
</div>)
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
import INotificationConnector from "./interfaces/INotificationConnector";
|
||||
import {deleteData, getData, postData} from "../App";
|
||||
import {deleteData, getData, putData} from "../App";
|
||||
import IGotifyRecord from "./interfaces/records/IGotifyRecord";
|
||||
import INtfyRecord from "./interfaces/records/INtfyRecord";
|
||||
import IlunaseaRecord from "./interfaces/records/IlunaseaRecord";
|
||||
|
||||
export default abstract class NotificationConnector {
|
||||
export default class NotificationConnector {
|
||||
|
||||
static async GetNotificationConnectors(apiUri: string) : Promise<INotificationConnector[]> {
|
||||
//console.info("Getting Notification Connectors");
|
||||
@ -11,154 +14,85 @@ export default abstract class NotificationConnector {
|
||||
const ret = json as INotificationConnector[];
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
})
|
||||
.catch(Promise.reject);
|
||||
});
|
||||
}
|
||||
|
||||
protected constructor() {
|
||||
|
||||
}
|
||||
|
||||
public abstract Test(apiUri: string) : Promise<boolean>;
|
||||
public abstract Reset(apiUri: string) : Promise<boolean>;
|
||||
public abstract Create(apiUri: string) : Promise<boolean>;
|
||||
protected abstract CheckConnector() : boolean;
|
||||
|
||||
protected async TestConnector(apiUri: string, connectorType: string, data: object): Promise<boolean> {
|
||||
if(!this.CheckConnector())
|
||||
return Promise.reject("Connector not fully configured.");
|
||||
//console.info(`Testing ${connectorType}`);
|
||||
return postData(`${apiUri}/v2/NotificationConnector/${connectorType}/Test`, data)
|
||||
static async CreateNotificationConnector(apiUri: string, newConnector: INotificationConnector): Promise<string> {
|
||||
return putData(`${apiUri}/v2/NotificationConnector`, newConnector)
|
||||
.then((json) => {
|
||||
//console.info(`Successfully tested ${connectorType}`);
|
||||
return true;
|
||||
})
|
||||
.catch(Promise.reject);
|
||||
//console.info("Got Notification Connectors");
|
||||
const ret = json as unknown as string;
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
protected async ResetConnector(apiUri: string, connectorType: string): Promise<boolean> {
|
||||
if(!this.CheckConnector())
|
||||
return Promise.reject("Connector not fully configured.");
|
||||
//console.info(`Deleting ${connectorType}`);
|
||||
return deleteData(`${apiUri}/v2/NotificationConnector/${connectorType}`)
|
||||
.then((json) => {
|
||||
//console.info(`Successfully deleted ${connectorType}`);
|
||||
return true;
|
||||
})
|
||||
.catch(Promise.reject);
|
||||
}
|
||||
|
||||
protected async CreateConnector(apiUri: string, connectorType: string, data: object): Promise<boolean> {
|
||||
if(!this.CheckConnector())
|
||||
return Promise.reject("Connector not fully configured.");
|
||||
//console.info(`Creating ${connectorType}`);
|
||||
return postData(`${apiUri}/v2/NotificationConnector/${connectorType}`, data)
|
||||
.then((json) => {
|
||||
//console.info(`Successfully created ${connectorType}`);
|
||||
return true;
|
||||
})
|
||||
.catch(Promise.reject);
|
||||
}
|
||||
}
|
||||
|
||||
export class Gotify extends NotificationConnector
|
||||
{
|
||||
public url = "";
|
||||
private appToken = "";
|
||||
|
||||
constructor({url, appToken} : {url: string, appToken:string}){
|
||||
super();
|
||||
this.url = url;
|
||||
this.appToken = appToken;
|
||||
}
|
||||
|
||||
public async Test(apiUri: string) : Promise<boolean> {
|
||||
return this.TestConnector(apiUri, "Gotify", {url: this.url, appToken: this.appToken}).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
public async Reset(apiUri: string) : Promise<boolean> {
|
||||
return this.ResetConnector(apiUri, "Gotify").then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
public async Create(apiUri: string) : Promise<boolean> {
|
||||
return this.CreateConnector(apiUri, "Gotify", {url: this.url, appToken: this.appToken}).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
protected CheckConnector(): boolean {
|
||||
try{
|
||||
new URL(this.url)
|
||||
}catch{
|
||||
return false;
|
||||
static async GetNotificationConnectorWithId(apiUri: string, notificationConnectorId: string) : Promise<INotificationConnector> {
|
||||
if(notificationConnectorId === undefined || notificationConnectorId === null || notificationConnectorId.length < 1) {
|
||||
console.error(`notificationConnectorId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
if(this.appToken.length < 1 || this.appToken.length < 1)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class Lunasea extends NotificationConnector
|
||||
{
|
||||
private webhook = "";
|
||||
|
||||
constructor({webhook} : {webhook: string}){
|
||||
super();
|
||||
this.webhook = webhook;
|
||||
//console.info("Getting Notification Connectors");
|
||||
return getData(`${apiUri}/v2/NotificationConnector/${notificationConnectorId}`)
|
||||
.then((json) => {
|
||||
//console.info("Got Notification Connectors");
|
||||
const ret = json as INotificationConnector;
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
public async Test(apiUri: string) : Promise<boolean> {
|
||||
return this.TestConnector(apiUri, "LunaSea", {webhook: this.webhook}).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
public async Reset(apiUri: string) : Promise<boolean> {
|
||||
return this.ResetConnector(apiUri, "LunaSea").then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
public async Create(apiUri: string) : Promise<boolean> {
|
||||
return this.CreateConnector(apiUri, "LunaSea", {webhook: this.webhook}).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
protected CheckConnector(): boolean {
|
||||
if(this.webhook.length < 1 || this.webhook.length < 1)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class Ntfy extends NotificationConnector
|
||||
{
|
||||
public url = "";
|
||||
private username = "";
|
||||
private password = "";
|
||||
public topic:string | undefined = undefined;
|
||||
|
||||
constructor({url, username, password, topic} : {url: string, username: string, password: string, topic : string | undefined}){
|
||||
super();
|
||||
this.url = url;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
public async Test(apiUri: string) : Promise<boolean> {
|
||||
return this.TestConnector(apiUri, "Ntfy", {url: this.url, username: this.username, password: this.password, topic: this.topic}).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
public async Reset(apiUri: string) : Promise<boolean> {
|
||||
return this.ResetConnector(apiUri, "Ntfy").then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
public async Create(apiUri: string) : Promise<boolean> {
|
||||
return this.CreateConnector(apiUri, "Ntfy", {url: this.url, username: this.username, password: this.password, topic: this.topic}).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
protected CheckConnector(): boolean {
|
||||
try{
|
||||
new URL(this.url)
|
||||
}catch{
|
||||
return false;
|
||||
static async DeleteNotificationConnectorWithId(apiUri: string, notificationConnectorId: string) : Promise<void> {
|
||||
if(notificationConnectorId === undefined || notificationConnectorId === null || notificationConnectorId.length < 1) {
|
||||
console.error(`notificationConnectorId was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
if(this.username.length < 1 || this.password.length < 1)
|
||||
return false;
|
||||
return true;
|
||||
//console.info("Getting Notification Connectors");
|
||||
return deleteData(`${apiUri}/v2/NotificationConnector/${notificationConnectorId}`);
|
||||
}
|
||||
|
||||
static async CreateGotify(apiUri: string, gotify: IGotifyRecord) : Promise<string> {
|
||||
if(gotify === undefined || gotify === null) {
|
||||
console.error(`gotify was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
//console.info("Getting Notification Connectors");
|
||||
return putData(`${apiUri}/v2/NotificationConnector/Gotify`, gotify)
|
||||
.then((json) => {
|
||||
//console.info("Got Notification Connectors");
|
||||
const ret = json as unknown as string;
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async CreateNtfy(apiUri: string, ntfy: INtfyRecord) : Promise<string> {
|
||||
if(ntfy === undefined || ntfy === null) {
|
||||
console.error(`ntfy was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
//console.info("Getting Notification Connectors");
|
||||
return putData(`${apiUri}/v2/NotificationConnector/Ntfy`, ntfy)
|
||||
.then((json) => {
|
||||
//console.info("Got Notification Connectors");
|
||||
const ret = json as unknown as string;
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async CreateLunasea(apiUri: string, lunasea: IlunaseaRecord) : Promise<string> {
|
||||
if(lunasea === undefined || lunasea === null) {
|
||||
console.error(`ntfy was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
//console.info("Getting Notification Connectors");
|
||||
return putData(`${apiUri}/v2/NotificationConnector/Lunasea`, lunasea)
|
||||
.then((json) => {
|
||||
//console.info("Got Notification Connectors");
|
||||
const ret = json as unknown as string;
|
||||
//console.debug(ret);
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import IJob from "./interfaces/IJob";
|
||||
import IJob, {JobState} from "./interfaces/IJob";
|
||||
import '../styles/queuePopUp.css';
|
||||
import '../styles/popup.css';
|
||||
import Job from "./Job";
|
||||
@ -8,10 +8,8 @@ import Manga from "./Manga";
|
||||
|
||||
export default function QueuePopUp({connectedToBackend, children, apiUri} : {connectedToBackend: boolean, children: JSX.Element[], apiUri: string}) {
|
||||
|
||||
const [StandbyJobs, setStandbyJobs] = React.useState<IJob[]>([]);
|
||||
const [StandbyJobsManga, setStandbyJobsManga] = React.useState<IManga[]>([]);
|
||||
const [WaitingJobs, setWaitingJobs] = React.useState<IJob[]>([]);
|
||||
const [RunningJobs, setRunningJobs] = React.useState<IJob[]>([]);
|
||||
const [RunningJobsManga, setRunningJobsManga] = React.useState<IManga[]>([]);
|
||||
const [showQueuePopup, setShowQueuePopup] = useState<boolean>(false);
|
||||
const [queueListInterval, setQueueListInterval] = React.useState<number>();
|
||||
|
||||
@ -34,49 +32,20 @@ export default function QueuePopUp({connectedToBackend, children, apiUri} : {con
|
||||
}, [connectedToBackend]);
|
||||
|
||||
function UpdateMonitoringJobsList(){
|
||||
Job.GetStandbyJobs(apiUri)
|
||||
.then((jobs:string[]) => {
|
||||
if(jobs.length > 0)
|
||||
return Job.GetJobs(apiUri, jobs);
|
||||
return [];
|
||||
})
|
||||
Job.GetJobsInState(apiUri, JobState.Waiting)
|
||||
.then((jobs:IJob[]) => {
|
||||
//console.debug("Removing Metadata Jobs");
|
||||
//console.log(StandbyJobs)
|
||||
setStandbyJobs(jobs.filter(job => job.jobType <= 2));
|
||||
setWaitingJobs(jobs);
|
||||
//console.log(StandbyJobs)
|
||||
});
|
||||
Job.GetRunningJobs(apiUri)
|
||||
.then((jobs:string[]) => {
|
||||
if(jobs.length > 0)
|
||||
return Job.GetJobs(apiUri, jobs);
|
||||
return [];
|
||||
})
|
||||
Job.GetJobsInState(apiUri, JobState.Running)
|
||||
.then((jobs:IJob[]) =>{
|
||||
//console.debug("Removing Metadata Jobs");
|
||||
setRunningJobs(jobs.filter(job => job.jobType <= 2));
|
||||
//console.log(StandbyJobs)
|
||||
setRunningJobs(jobs);
|
||||
//console.log(StandbyJobs)
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(StandbyJobs.length < 1)
|
||||
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(apiUri, mangaIds)
|
||||
.then(setStandbyJobsManga);
|
||||
}, [StandbyJobs]);
|
||||
|
||||
useEffect(() => {
|
||||
if(RunningJobs.length < 1)
|
||||
return;
|
||||
//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(apiUri, mangaIds)
|
||||
.then(setRunningJobsManga);
|
||||
}, [RunningJobs]);
|
||||
|
||||
return (<>
|
||||
<div onClick={() => setShowQueuePopup(true)}>
|
||||
{children}
|
||||
@ -89,28 +58,6 @@ export default function QueuePopUp({connectedToBackend, children, apiUri} : {con
|
||||
onClick={() => setShowQueuePopup(false)}/>
|
||||
</div>
|
||||
<div id="QueuePopUpBody" className="popupBody">
|
||||
<div id="RunningJobQueue">
|
||||
<h1>Running</h1>
|
||||
<div className="JobQueue">
|
||||
{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 QueueItem(apiUri, manga, job, UpdateMonitoringJobsList);
|
||||
})}
|
||||
</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 QueueItem(apiUri, manga, job, UpdateMonitoringJobsList);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: <></>
|
||||
|
@ -5,6 +5,7 @@ import {isValidUri} from "../App";
|
||||
import IManga, {SearchResult} from "./interfaces/IManga";
|
||||
import '../styles/search.css';
|
||||
import '../styles/MangaSearchResult.css'
|
||||
import SearchFunctions from "./SearchFunctions";
|
||||
|
||||
export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch} : {apiUri: string, jobInterval: Date, onJobsChanged: (internalId: string) => void, closeSearch(): void}) {
|
||||
const [mangaConnectors, setConnectors] = useState<IMangaConnector[]>();
|
||||
@ -17,7 +18,7 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
||||
|
||||
useEffect(() => {
|
||||
if(mangaConnectors === undefined) {
|
||||
MangaConnector.GetAllConnectors().then(setConnectors);
|
||||
MangaConnector.GetAllConnectors(apiUri).then(setConnectors)
|
||||
return;
|
||||
}
|
||||
}, [mangaConnectors]);
|
||||
@ -30,7 +31,7 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
||||
if(selectedConnector === undefined)
|
||||
return;
|
||||
setSelectedConnector(selectedConnector);
|
||||
setSelectedLanguage(selectedConnector.SupportedLanguages[0]);
|
||||
setSelectedLanguage(selectedConnector.supportedLanguages[0]);
|
||||
}
|
||||
|
||||
const searchBoxValueChanged : ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
@ -46,11 +47,11 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
||||
return;
|
||||
let baseUri = match[1];
|
||||
const selectCon = mangaConnectors.find((con: IMangaConnector) => {
|
||||
return con.BaseUris.find(uri => uri == baseUri);
|
||||
return con.baseUris.find(uri => uri == baseUri);
|
||||
});
|
||||
if(selectCon != undefined){
|
||||
setSelectedConnector(selectCon);
|
||||
setSelectedLanguage(selectCon.SupportedLanguages[0]);
|
||||
setSelectedLanguage(selectCon.supportedLanguages[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +61,7 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
||||
return;
|
||||
}
|
||||
//console.info(`Searching Name: ${searchBoxValue} Connector: ${selectedConnector.name} Language: ${selectedLanguage}`);
|
||||
if(isValidUri(searchBoxValue) && !selectedConnector.BaseUris.find((uri: string) => {
|
||||
if(isValidUri(searchBoxValue) && !selectedConnector.baseUris.find((uri: string) => {
|
||||
const match = searchBoxValue.match(pattern);
|
||||
if(match === null)
|
||||
return false;
|
||||
@ -71,12 +72,12 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
||||
return;
|
||||
}
|
||||
if(!isValidUri(searchBoxValue)){
|
||||
MangaConnector.GetMangaFromConnectorByTitle(selectedConnector, searchBoxValue)
|
||||
SearchFunctions.SearchNameOnConnector(apiUri, selectedConnector.name, searchBoxValue)
|
||||
.then((mangas: IManga[]) => {
|
||||
setSearchResults(mangas);
|
||||
});
|
||||
}else{
|
||||
MangaConnector.GetMangaFromConnectorByUrl(selectedConnector, searchBoxValue)
|
||||
SearchFunctions.SearchUrl(apiUri, searchBoxValue)
|
||||
.then((manga: IManga) => {
|
||||
setSearchResults([manga]);
|
||||
});
|
||||
@ -97,7 +98,7 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
||||
<select id="Searchbox-language" onChange={changeSelectedLanguage} value={selectedLanguage === null ? "" : selectedLanguage}>
|
||||
{selectedConnector === undefined
|
||||
? <option value="" disabled hidden>Select Connector</option>
|
||||
: selectedConnector.SupportedLanguages.map(language => <option value={language} key={language}>{language}</option>)}
|
||||
: selectedConnector.supportedLanguages.map(language => <option value={language} key={language}>{language}</option>)}
|
||||
</select>
|
||||
<button id="Searchbox-button" type="submit" onClick={ExecuteSearch}>Search</button>
|
||||
</div>
|
||||
@ -107,5 +108,5 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
||||
? <p></p>
|
||||
: searchResults.map(result => SearchResult(apiUri, result, jobInterval, onJobsChanged))}
|
||||
</div>
|
||||
</div>)
|
||||
</div>);
|
||||
}
|
46
Website/modules/SearchFunctions.tsx
Normal file
46
Website/modules/SearchFunctions.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import IManga from "./interfaces/IManga";
|
||||
import {getData, postData} from "../App";
|
||||
|
||||
export default class SearchFunctions {
|
||||
|
||||
static async SearchName(apiUri: string, name: string) : Promise<IManga[]> {
|
||||
if(name === undefined || name === null || name.length < 1) {
|
||||
console.error(`name was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return postData(`${apiUri}/v2/Search/Name`, name)
|
||||
.then((json) => {
|
||||
const ret = json as IManga[];
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async SearchNameOnConnector(apiUri: string, connectorName: string, name: string) : Promise<IManga[]> {
|
||||
if(connectorName === undefined || connectorName === null || connectorName.length < 1) {
|
||||
console.error(`connectorName was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
if(name === undefined || name === null || name.length < 1) {
|
||||
console.error(`name was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return postData(`${apiUri}/v2/Search/${connectorName}`, name)
|
||||
.then((json) => {
|
||||
const ret = json as IManga[];
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
static async SearchUrl(apiUri: string, url: string) : Promise<IManga> {
|
||||
if(url === undefined || url === null || url.length < 1) {
|
||||
console.error(`name was not provided`);
|
||||
return Promise.reject();
|
||||
}
|
||||
return postData(`${apiUri}/v2/Search/Url`, url)
|
||||
.then((json) => {
|
||||
const ret = json as IManga;
|
||||
return (ret);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -1,318 +1,7 @@
|
||||
import React, {ChangeEventHandler, KeyboardEventHandler, MouseEventHandler, useEffect, useState} from 'react';
|
||||
import IFrontendSettings from "./interfaces/IFrontendSettings";
|
||||
import '../styles/settings.css';
|
||||
import IBackendSettings from "./interfaces/IBackendSettings";
|
||||
import {getData, postData} from "../App";
|
||||
import LibraryConnector, {Kavita, Komga} from "./LibraryConnector";
|
||||
import NotificationConnector, {Gotify, Lunasea, Ntfy} from "./NotificationConnector";
|
||||
import ILibraryConnector from "./interfaces/ILibraryConnector";
|
||||
import INotificationConnector from "./interfaces/INotificationConnector";
|
||||
import Toggle from 'react-toggle';
|
||||
import '../styles/react-toggle.css';
|
||||
|
||||
export default function Settings({backendConnected, apiUri, settings, changeSettings} : {backendConnected: boolean, apiUri: string, settings: IFrontendSettings, changeSettings: (settings: IFrontendSettings) => void}) {
|
||||
const [frontendSettings, setFrontendSettings] = useState<IFrontendSettings>(settings);
|
||||
const [backendSettings, setBackendSettings] = useState<IBackendSettings>();
|
||||
const [showSettings, setShowSettings] = useState<boolean>(false);
|
||||
const [libraryConnectors, setLibraryConnectors] = useState<ILibraryConnector[]>();
|
||||
const [notificationConnectors, setNotificationConnectors] = useState<INotificationConnector[]>();
|
||||
const [komgaSettings, setKomgaSettings] = useState<{url: string, username: string, password: string}>({url: "", username: "", password: ""});
|
||||
const [kavitaSettings, setKavitaSettings] = useState<{url: string, username: string, password: string}>({url: "", username: "", password: ""});
|
||||
const [gotifySettings, setGotifySettings] = useState<{url: string, appToken: string}>({url: "", appToken: ""});
|
||||
const [lunaseaSettings, setLunaseaSettings] = useState<{webhook: string}>({webhook: ""});
|
||||
const [ntfySettings, setNtfySettings] = useState<{url: string, username: string, password: string, topic: string | undefined}>({url: "", username: "", password: "", topic: undefined});
|
||||
|
||||
useEffect(() => {
|
||||
console.debug(`${showSettings ? "Showing" : "Not showing"} settings.`);
|
||||
if(!showSettings || !backendConnected)
|
||||
return;
|
||||
UpdateBackendSettings();
|
||||
LibraryConnector.GetLibraryConnectors(apiUri).then(setLibraryConnectors).catch(console.error);
|
||||
NotificationConnector.GetNotificationConnectors(apiUri).then(setNotificationConnectors).catch(console.error);
|
||||
}, [showSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
changeSettings(frontendSettings);
|
||||
}, [frontendSettings]);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function UpdateBackendSettings() {
|
||||
GetSettings(apiUri).then(setBackendSettings).catch(console.error);
|
||||
}
|
||||
|
||||
const GetKomga = () : ILibraryConnector | undefined =>
|
||||
libraryConnectors?.find(con => con.libraryType == 0);
|
||||
|
||||
const KomgaConnected = () : boolean => GetKomga() != undefined;
|
||||
|
||||
const GetKavita = () : ILibraryConnector | undefined =>
|
||||
libraryConnectors?.find(con => con.libraryType == 1);
|
||||
|
||||
const KavitaConnected = () : boolean => GetKavita() != undefined;
|
||||
|
||||
const GetGotify = () : INotificationConnector | undefined =>
|
||||
notificationConnectors?.find(con => con.notificationConnectorType == 0);
|
||||
|
||||
const GotifyConnected = () : boolean => GetGotify() != undefined;
|
||||
|
||||
const GetLunasea = () : INotificationConnector | undefined =>
|
||||
notificationConnectors?.find(con => con.notificationConnectorType == 1);
|
||||
|
||||
const LunaseaConnected = () : boolean => GetLunasea() != undefined;
|
||||
|
||||
const GetNtfy = () : INotificationConnector | undefined =>
|
||||
notificationConnectors?.find(con => con.notificationConnectorType == 2);
|
||||
|
||||
const NtfyConnected = () : boolean => GetNtfy() != undefined;
|
||||
|
||||
const SubmitApiUri : KeyboardEventHandler<HTMLInputElement> = (e) => {
|
||||
if(e.currentTarget.value.length < 1)
|
||||
return;
|
||||
if(e.key == "Enter"){
|
||||
setFrontendSettings({...frontendSettings, apiUri: e.currentTarget.value});
|
||||
RefreshInputs();
|
||||
}
|
||||
}
|
||||
|
||||
const SubmitUserAgent: KeyboardEventHandler<HTMLInputElement> = (e) => {
|
||||
if(e.currentTarget.value.length < 1 || backendSettings === undefined)
|
||||
return;
|
||||
if(e.key == "Enter"){
|
||||
//console.info(`Updating Useragent ${e.currentTarget.value}`);
|
||||
postData(`${apiUri}/v2/Settings/UserAgent`, {value: e.currentTarget.value})
|
||||
.then((json) => {
|
||||
//console.info(`Successfully updated Useragent ${e.currentTarget.value}`);
|
||||
UpdateBackendSettings();
|
||||
RefreshInputs();
|
||||
})
|
||||
.catch(() => alert("Failed to update Useragent."));
|
||||
}
|
||||
}
|
||||
|
||||
const ResetUserAgent: MouseEventHandler<HTMLSpanElement> = () => {
|
||||
//console.info(`Resetting Useragent`);
|
||||
postData(`${apiUri}/v2/Settings/UserAgent`, {value: undefined})
|
||||
.then((json) => {
|
||||
//console.info(`Successfully reset Useragent`);
|
||||
UpdateBackendSettings();
|
||||
RefreshInputs();
|
||||
})
|
||||
.catch(() => alert("Failed to update Useragent."));
|
||||
}
|
||||
|
||||
const SetAprilFoolsMode : ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
//console.info(`Updating AprilFoolsMode ${e.target.checked}`);
|
||||
postData(`${apiUri}/v2/Settings/AprilFoolsMode`, {value: e.target.checked})
|
||||
.then((json) => {
|
||||
//console.info(`Successfully updated AprilFoolsMode ${e.currentTarget.value}`);
|
||||
UpdateBackendSettings();
|
||||
})
|
||||
}
|
||||
|
||||
const SetCompressImages : MouseEventHandler<HTMLInputElement> = (e) => {
|
||||
//console.info(`Updating ImageCompression ${e.currentTarget.value}`);
|
||||
postData(`${apiUri}/v2/Settings/CompressImages`, {value: e.currentTarget.value})
|
||||
.then((json) => {
|
||||
//console.info(`Successfully updated ImageCompression ${e.currentTarget.value}`);
|
||||
UpdateBackendSettings();
|
||||
})
|
||||
}
|
||||
|
||||
const SetBWImages : ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
//console.info(`Updating B/W Images ${e.target.checked}`);
|
||||
postData(`${apiUri}/v2/Settings/BWImages`, {value: e.target.checked})
|
||||
.then((json) => {
|
||||
//console.info(`Successfully updated B/W Images ${e.target.checked}`);
|
||||
UpdateBackendSettings();
|
||||
})
|
||||
}
|
||||
|
||||
function RefreshInputs(){
|
||||
alert("Saved.");
|
||||
setShowSettings(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Settings">
|
||||
<img id="SettingsIcon" src="../media/settings-cogwheel.svg" alt="cogwheel" onClick={() => setShowSettings(true)}/>
|
||||
{showSettings
|
||||
? <div className="popup">
|
||||
<div className="popupHeader">
|
||||
<h1>Settings</h1>
|
||||
<img alt="Close Settings" className="close" src="../media/close-x.svg" onClick={() => setShowSettings(false)}/>
|
||||
</div>
|
||||
<div id="settingsPopupBody" className="popupBody">
|
||||
<div className="settings-section">
|
||||
TRANGA
|
||||
<div className="settings-section-content">
|
||||
<div className="section-item">
|
||||
<span className="settings-section-title">API Settings</span>
|
||||
<label className="tooltip" data-tooltip="Set the URI of the Backend. Include https:// and if necessary port." htmlFor="settingApiUri">API URI:</label>
|
||||
<input placeholder={frontendSettings.apiUri} type="text" id="settingApiUri"
|
||||
onKeyDown={SubmitApiUri}/>
|
||||
<label htmlFor="userAgent" className="tooltip" data-tooltip="Set a custom User-Agent. This will allow more frequent requests to sites.">User Agent:</label>
|
||||
<input id="userAgent" type="text"
|
||||
placeholder={backendSettings != undefined ? backendSettings.userAgent : "UserAgent"}
|
||||
onKeyDown={SubmitUserAgent}/>
|
||||
<span id="resetUserAgent" onClick={ResetUserAgent}>Reset</span>
|
||||
<label htmlFor="aprilFoolsMode" className="tooltip" data-tooltip="Disable all downloads on April 1st">April Fools Mode</label>
|
||||
<Toggle id="aprilFoolsMode"
|
||||
checked={backendSettings?.aprilFoolsMode ?? false}
|
||||
onChange={SetAprilFoolsMode}/>
|
||||
<label htmlFor="compression" className="tooltip" data-tooltip="JPEG Compression Quality">Image Compression</label>
|
||||
<input type="range" min="1" max="100" defaultValue={backendSettings?.compression ?? 50} className="slider" id="compression" onMouseUp={SetCompressImages}/>
|
||||
<label htmlFor="bwImages">B/W Images</label>
|
||||
<Toggle id="bwImages"
|
||||
checked={backendSettings?.bwImages ?? false}
|
||||
onChange={SetBWImages}/>
|
||||
</div>
|
||||
<div className="section-item">
|
||||
<span className="settings-section-title">Rate Limits</span>
|
||||
<label htmlFor="DefaultRL">Default:</label>
|
||||
<input id="defaultRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.Default.toString() : "-1"} />
|
||||
<label htmlFor="CoverRL">Manga Covers:</label>
|
||||
<input id="coverRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaCover.toString() : "-1"} />
|
||||
<label htmlFor="ImageRL">Manga Images:</label>
|
||||
<input id="imageRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaImage.toString() : "-1"} />
|
||||
<label htmlFor="InfoRL">Manga Info:</label>
|
||||
<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>
|
||||
<label htmlFor="cssStyle">Library Style:</label>
|
||||
<select id="cssStyle">
|
||||
<option id="card_compact" value="card_compact">Cards (Compact)</option>
|
||||
<option id="card_hover" value="card_hover">Cards (Hover)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<span className="settings-section-title">Sources</span>
|
||||
<div className="settings-section-content">
|
||||
<div className="section-item">
|
||||
<span className="settings-section-title">
|
||||
<img src="../media/connector-icons/mangadex-logo.svg" alt="Mangadex Logo" />
|
||||
<a href="https://mangadex.org">MangaDex</a>
|
||||
</span>
|
||||
<label htmlFor="mDexFeedRL">Feed Rate Limit:</label>
|
||||
<input id="mDexFeedRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaDexFeed.toString() : "-1"} />
|
||||
<label htmlFor="mDexImageRL">Image Rate Limit:</label>
|
||||
<input id="mDexImageRL" type="number" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaDexImage.toString() : "-1"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section" >
|
||||
LIBRARY CONNECTORS
|
||||
<div className="settings-section-content">
|
||||
<div className="section-item" connector-status={KomgaConnected() ? "Configured" : "Not Configured"}>
|
||||
<span className="settings-section-title">
|
||||
<img src='../media/connector-icons/komga.svg' alt="Komga Logo"/>
|
||||
Komga
|
||||
</span>
|
||||
<label htmlFor="komgaUrl">URL</label>
|
||||
<input placeholder={GetKomga()?.baseUrl ?? "URL"} id="komgaUrl" type="text" onChange={(e) => setKomgaSettings(s => ({...s, url: e.target.value}))} />
|
||||
<label htmlFor="komgaUsername">Username</label>
|
||||
<input placeholder={KomgaConnected() ? "***" : "Username"} id="komgaUsername" type="text" onChange={(e) => setKomgaSettings(s => ({...s, username: e.target.value}))} />
|
||||
<label htmlFor="komgaPassword">Password</label>
|
||||
<input placeholder={KomgaConnected() ? "***" : "Password"} id="komgaPassword" type="password" onChange={(e) => setKomgaSettings(s => ({...s, password: e.target.value}))} />
|
||||
<div className="section-actions">
|
||||
<span onClick={() => new Komga(komgaSettings).Test(apiUri).then(()=>alert("Test successful"))}>Test</span>
|
||||
<span onClick={() => new Komga(komgaSettings).Reset(apiUri).then(RefreshInputs)}>Reset</span>
|
||||
<span onClick={() => new Komga(komgaSettings).Create(apiUri).then(RefreshInputs)}>Apply</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section-item" connector-status={KavitaConnected() ? "Configured" : "Not Configured" }>
|
||||
<span className="settings-section-title">
|
||||
<img src='../media/connector-icons/kavita.png' alt="Kavita Logo"/>
|
||||
Kavita
|
||||
</span>
|
||||
<label htmlFor="kavitaUrl">URL</label>
|
||||
<input placeholder={GetKavita()?.baseUrl ?? "URL"} id="kavitaUrl" type="text" onChange={(e) => setKavitaSettings(s => ({...s, url: e.target.value}))} />
|
||||
<label htmlFor="kavitaUsername">Username</label>
|
||||
<input placeholder={KavitaConnected() ? "***" : "Username"} id="kavitaUsername" type="text" onChange={(e) => setKavitaSettings(s => ({...s, username: e.target.value}))} />
|
||||
<label htmlFor="kavitaPassword">Password</label>
|
||||
<input placeholder={KavitaConnected() ? "***" : "Password"} id="kavitaPassword" type="password" onChange={(e) => setKavitaSettings(s => ({...s, password: e.target.value}))} />
|
||||
<div className="section-actions">
|
||||
<span onClick={() => new Kavita(kavitaSettings).Test(apiUri).then(()=>alert("Test successful"))}>Test</span>
|
||||
<span onClick={() => new Kavita(kavitaSettings).Reset(apiUri).then(RefreshInputs)}>Reset</span>
|
||||
<span onClick={() => new Kavita(kavitaSettings).Create(apiUri).then(RefreshInputs)}>Apply</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
NOTIFICATION CONNECTORS
|
||||
<div className="settings-section-content">
|
||||
<div className="section-item" connector-status={GotifyConnected() ? "Configured" : "Not Configured"}>
|
||||
<span className="settings-section-title">
|
||||
<img src='../media/connector-icons/gotify-logo.png' alt="Gotify Logo"/>
|
||||
Gotify
|
||||
</span>
|
||||
<label htmlFor="gotifyUrl">URL</label>
|
||||
<input placeholder={GetGotify()?.endpoint ?? "URL"} id="gotifyUrl" type="text" onChange={(e) => setGotifySettings(s => ({...s, url: e.target.value}))} />
|
||||
<label htmlFor="gotifyAppToken">AppToken</label>
|
||||
<input placeholder={GotifyConnected() ? "***" : "AppToken"} id="gotifyAppToken" type="text" onChange={(e) => setGotifySettings(s => ({...s, appToken: e.target.value}))} />
|
||||
<div className="section-actions">
|
||||
<span onClick={() => new Gotify(gotifySettings).Test(apiUri).then(()=>alert("Test successful"))}>Test</span>
|
||||
<span onClick={() => new Gotify(gotifySettings).Reset(apiUri).then(RefreshInputs)}>Reset</span>
|
||||
<span onClick={() => new Gotify(gotifySettings).Create(apiUri).then(RefreshInputs)}>Apply</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section-item"
|
||||
connector-status={LunaseaConnected() ? "Configured" : "Not Configured"}>
|
||||
<span className="settings-section-title">
|
||||
<img src='../media/connector-icons/lunasea.png' alt="Lunasea Logo"/>
|
||||
LunaSea
|
||||
</span>
|
||||
<label htmlFor="lunaseaWebhook">Webhook id</label>
|
||||
<input placeholder={GetLunasea() != undefined ? "***" : "device/:id or user/:id"} id="lunaseaWebhook" type="text" onChange={(e) => setLunaseaSettings(s => ({...s, webhook: e.target.value}))} />
|
||||
<div className="section-actions">
|
||||
<span onClick={() => new Lunasea(lunaseaSettings).Test(apiUri).then(()=>alert("Test successful"))}>Test</span>
|
||||
<span onClick={() => new Lunasea(lunaseaSettings).Reset(apiUri).then(RefreshInputs)}>Reset</span>
|
||||
<span onClick={() => new Lunasea(lunaseaSettings).Create(apiUri).then(RefreshInputs)}>Apply</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section-item"
|
||||
connector-status={NtfyConnected() ? "Configured" : "Not Configured"}>
|
||||
<span className="settings-section-title">
|
||||
<img src='../media/connector-icons/ntfy.svg' alt="ntfy Logo"/>
|
||||
Ntfy
|
||||
</span>
|
||||
<label htmlFor="ntfyEndpoint">URL</label>
|
||||
<input placeholder={GetNtfy()?.endpoint ?? "URL"} id="ntfyEndpoint" type="text" onChange={(e) => setNtfySettings(s => ({...s, url: e.target.value}))} />
|
||||
<label htmlFor="ntfyUsername">Username</label>
|
||||
<input placeholder={NtfyConnected() ? "***" : "Username"} id="ntfyUsername" type="text" onChange={(e) => setNtfySettings(s => ({...s, username: e.target.value}))} />
|
||||
<label htmlFor="ntfyPassword">Password</label>
|
||||
<input placeholder={NtfyConnected() ? "***" : "Password"} id="ntfyPassword" type="password" onChange={(e) => setNtfySettings(s => ({...s, password: e.target.value}))} />
|
||||
<label htmlFor="ntfyTopic">Topic</label>
|
||||
<input placeholder={GetNtfy()?.topic ?? "Topic"} id="ntfyTopic" type="text" onChange={(e) => setNtfySettings(s => ({...s, topic: e.target.value}))} />
|
||||
<div className="section-actions">
|
||||
<span onClick={() => new Ntfy(ntfySettings).Test(apiUri).then(()=>alert("Test successful"))}>Test</span>
|
||||
<span onClick={() => new Ntfy(ntfySettings).Reset(apiUri).then(RefreshInputs)}>Reset</span>
|
||||
<span onClick={() => new Ntfy(ntfySettings).Create(apiUri).then(RefreshInputs)}>Apply</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
21
Website/modules/interfaces/IAuthor.tsx
Normal file
21
Website/modules/interfaces/IAuthor.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React, {ReactElement, useEffect} from "react";
|
||||
import {getData} from "../../App";
|
||||
|
||||
export default interface IAuthor {
|
||||
authorId: string;
|
||||
authorName: string;
|
||||
}
|
||||
|
||||
export function AuthorElement({apiUri, authorId} : {apiUri: string, authorId: string}) : ReactElement{
|
||||
let [name, setName] = React.useState<string>(authorId);
|
||||
|
||||
useEffect(()=> {
|
||||
getData(`${apiUri}/v2/Query/Author/${authorId}`)
|
||||
.then((json) => {
|
||||
let ret = json as IAuthor;
|
||||
setName(ret.authorName);
|
||||
});
|
||||
}, [])
|
||||
|
||||
return (<span>{name}</span>);
|
||||
}
|
@ -1,11 +1,6 @@
|
||||
export default interface IBackendSettings {
|
||||
"downloadLocation": string;
|
||||
"workingDirectory": string;
|
||||
"apiPortNumber": number;
|
||||
"userAgent": string;
|
||||
"bufferLibraryUpdates": boolean;
|
||||
"bufferNotifications": boolean;
|
||||
"version": number;
|
||||
"aprilFoolsMode": boolean;
|
||||
"compression": number;
|
||||
"bwImages": boolean;
|
10
Website/modules/interfaces/IChapter.ts
Normal file
10
Website/modules/interfaces/IChapter.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export default interface IChapter{
|
||||
chapterId: string;
|
||||
volumeNumber: number;
|
||||
chapterNumber: string;
|
||||
url: string;
|
||||
title: string | undefined;
|
||||
archiveFileName: string;
|
||||
downloaded: boolean;
|
||||
parentMangaId: string;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import IManga from "./IManga";
|
||||
|
||||
export default interface IChapter{
|
||||
parentManga: IManga;
|
||||
name: string | undefined;
|
||||
volumeNumber: string;
|
||||
chapterNumber: string;
|
||||
url: string;
|
||||
fileName: string;
|
||||
}
|
28
Website/modules/interfaces/IJob.ts
Normal file
28
Website/modules/interfaces/IJob.ts
Normal file
@ -0,0 +1,28 @@
|
||||
export default interface IJob{
|
||||
jobId: string;
|
||||
parentJobId: string;
|
||||
dependsOnJobIds: string[];
|
||||
jobType: JobType;
|
||||
recurrenceMs: number;
|
||||
lastExecution: Date;
|
||||
nextExecution: Date;
|
||||
state: JobState;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export enum JobType {
|
||||
DownloadSingleChapterJob = "DownloadSingleChapterJob",
|
||||
DownloadAvailableChaptersJob = "DownloadAvailableChaptersJob",
|
||||
UpdateMetaDataJob = "UpdateMetaDataJob",
|
||||
MoveFileOrFolderJob = "MoveFileOrFolderJob",
|
||||
DownloadMangaCoverJob = "DownloadMangaCoverJob",
|
||||
RetrieveChaptersJob = "RetrieveChaptersJob",
|
||||
UpdateFilesDownloadedJob = "UpdateFilesDownloadedJob"
|
||||
}
|
||||
|
||||
export enum JobState {
|
||||
Waiting = "Waiting",
|
||||
Running = "Running",
|
||||
Completed = "Completed",
|
||||
Failed = "Failed"
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import IMangaConnector from "./IMangaConnector";
|
||||
import IProgressToken from "./IProgressToken";
|
||||
import IChapter from "./IChapter";
|
||||
|
||||
export default interface IJob{
|
||||
progressToken: IProgressToken;
|
||||
recurring: boolean;
|
||||
recurrenceTime: string;
|
||||
lastExecution: Date;
|
||||
nextExecution: Date;
|
||||
id: string;
|
||||
jobType: number;
|
||||
parentJobId: string | null;
|
||||
mangaConnector: IMangaConnector;
|
||||
mangaInternalId: string | undefined; //only on DownloadNewChapters
|
||||
translatedLanguage: string | undefined; //only on DownloadNewChapters
|
||||
chapter: IChapter | undefined; //only on DownloadChapter
|
||||
}
|
||||
|
||||
export function JobTypeFromNumber(n: number): string {
|
||||
switch(n) {
|
||||
case 0: return "Download Chapter";
|
||||
case 1: return "Download New Chapters";
|
||||
case 2: return "Update Metadata";
|
||||
case 3: return "Monitor";
|
||||
}
|
||||
return "";
|
||||
}
|
11
Website/modules/interfaces/ILibraryConnector.ts
Normal file
11
Website/modules/interfaces/ILibraryConnector.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export default interface ILibraryConnector {
|
||||
libraryConnectorId: string;
|
||||
libraryType: LibraryType;
|
||||
baseUrl: string;
|
||||
auth: string;
|
||||
}
|
||||
|
||||
export enum LibraryType {
|
||||
Komga = "Komga",
|
||||
Kavita = "Kavita"
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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 "";
|
||||
}
|
25
Website/modules/interfaces/ILink.tsx
Normal file
25
Website/modules/interfaces/ILink.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React, {ReactElement, useEffect} from "react";
|
||||
import {getData} from "../../App";
|
||||
import IAuthor from "./IAuthor";
|
||||
|
||||
export default interface ILink {
|
||||
linkId: string;
|
||||
linkProvider: string;
|
||||
linkUrl: string;
|
||||
}
|
||||
|
||||
export function LinkElement({apiUri, linkId} : {apiUri: string, linkId: string}) : ReactElement{
|
||||
let [provider, setProvider] = React.useState<string>(linkId);
|
||||
let [linkUrl, setLinkUrl] = React.useState<string>("");
|
||||
|
||||
useEffect(()=> {
|
||||
getData(`${apiUri}/v2/Query/Link/${linkId}`)
|
||||
.then((json) => {
|
||||
let ret = json as ILink;
|
||||
setProvider(ret.linkProvider);
|
||||
setLinkUrl(ret.linkUrl);
|
||||
});
|
||||
}, [])
|
||||
|
||||
return (<a href={linkUrl}>{provider}</a>);
|
||||
}
|
@ -1,126 +1,104 @@
|
||||
import IMangaConnector from "./IMangaConnector";
|
||||
import KeyValuePair from "./KeyValuePair";
|
||||
import Manga from "../Manga";
|
||||
import React, {EventHandler, ReactElement, ReactEventHandler} from "react";
|
||||
import React, {ReactElement, ReactEventHandler} from "react";
|
||||
import Icon from '@mdi/react';
|
||||
import { mdiTagTextOutline, mdiAccountEdit } from '@mdi/js';
|
||||
import { mdiTagTextOutline, mdiAccountEdit, mdiLinkVariant } from '@mdi/js';
|
||||
import MarkdownPreview from '@uiw/react-markdown-preview';
|
||||
import IJob, {JobTypeFromNumber} from "./IJob";
|
||||
import IJob from "./IJob";
|
||||
import {AuthorElement} from "./IAuthor";
|
||||
import Job from "../Job";
|
||||
import ProgressBar from "@ramonak/react-progress-bar";
|
||||
import {LinkElement} from "./ILink";
|
||||
|
||||
export default interface IManga{
|
||||
"sortName": string,
|
||||
"authors": string[],
|
||||
"altTitles": KeyValuePair[],
|
||||
"description": string,
|
||||
"tags": string[],
|
||||
"coverUrl": string,
|
||||
"coverFileNameInCache": string,
|
||||
"links": KeyValuePair[],
|
||||
"year": number,
|
||||
"originalLanguage": string,
|
||||
"releaseStatus": number,
|
||||
"folderName": string,
|
||||
"publicationId": string,
|
||||
"internalId": string,
|
||||
"ignoreChaptersBelow": number,
|
||||
"latestChapterDownloaded": number,
|
||||
"latestChapterAvailable": number,
|
||||
"websiteUrl": string,
|
||||
"mangaConnector": IMangaConnector
|
||||
mangaId: string;
|
||||
connectorId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
websiteUrl: string;
|
||||
year: number;
|
||||
originalLanguage: string;
|
||||
releaseStatus: MangaReleaseStatus;
|
||||
folderName: string;
|
||||
ignoreChapterBefore: number;
|
||||
mangaConnectorId: string;
|
||||
authorIds: string[];
|
||||
tags: string[];
|
||||
linkIds: string[];
|
||||
altTitleIds: string[];
|
||||
}
|
||||
|
||||
export function ReleaseStatusFromNumber(n: number): string {
|
||||
switch(n) {
|
||||
case 0: return "Ongoing";
|
||||
case 1: return "Completed";
|
||||
case 2: return "OnHiatus";
|
||||
case 3: return "Cancelled";
|
||||
case 4: return "Unreleased";
|
||||
}
|
||||
return "";
|
||||
export enum MangaReleaseStatus {
|
||||
Continuing = "Continuing",
|
||||
Completed = "Completed",
|
||||
OnHiatus = "OnHiatus",
|
||||
Cancelled = "Cancelled",
|
||||
Unreleased = "Unreleased",
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function CoverCard(apiUri: string, manga: IManga) : ReactElement {
|
||||
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
|
||||
if(e.currentTarget.src != Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget))
|
||||
e.currentTarget.src = Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget);
|
||||
}
|
||||
|
||||
return(
|
||||
<div className="Manga" key={manga.internalId}>
|
||||
<img src="../../media/blahaj.png" onLoad={MangaCover} alt="Manga Cover"></img>
|
||||
<div className="Manga" key={manga.mangaId}>
|
||||
<img src="../../media/blahaj.png" alt="Manga Cover"></img>
|
||||
<div>
|
||||
<p className="pill connector-name">{manga.mangaConnector.name}</p>
|
||||
<div className="Manga-status" release-status={ReleaseStatusFromNumber(manga.releaseStatus)}></div>
|
||||
<p className="Manga-name">{manga.sortName}</p>
|
||||
<p className="pill connector-name">{manga.mangaConnectorId}</p>
|
||||
<div className="Manga-status" release-status={manga.releaseStatus}></div>
|
||||
<p className="Manga-name">{manga.name}</p>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
export function SearchResult(apiUri: string, manga: IManga, interval: Date, onJobsChanged: (internalId: string) => void) : ReactElement {
|
||||
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
|
||||
if(e.currentTarget.src != Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget))
|
||||
e.currentTarget.src = Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget);
|
||||
console.log(manga.mangaId);
|
||||
if(e.currentTarget.src != Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget))
|
||||
e.currentTarget.src = Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget);
|
||||
}
|
||||
|
||||
return(
|
||||
<div className="SearchResult" key={manga.internalId}>
|
||||
<img src="../../media/blahaj.png" onLoad={MangaCover} alt="Manga Cover"></img>
|
||||
<p className="connector-name">{manga.mangaConnector.name}</p>
|
||||
<div className="Manga-status" release-status={ReleaseStatusFromNumber(manga.releaseStatus)}></div>
|
||||
<p className="Manga-name"><a href={manga.websiteUrl}>{manga.sortName}<img src="../../media/link.svg"
|
||||
<div className="SearchResult" key={manga.mangaId}>
|
||||
<img src={Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, undefined)} alt="Manga Cover" onLoad={MangaCover}></img>
|
||||
<p className="connector-name">{manga.mangaConnectorId}</p>
|
||||
<div className="Manga-status" release-status={manga.releaseStatus}></div>
|
||||
<p className="Manga-name"><a href={manga.websiteUrl}>{manga.name}<img src="../../media/link.svg"
|
||||
alt=""/></a></p>
|
||||
<div className="Manga-tags">
|
||||
{manga.authors.map(author => <p className="Manga-author" key={manga.internalId + "-author-" + author}>
|
||||
<Icon path={mdiAccountEdit} size={0.5}/> {author}</p>)}
|
||||
{manga.tags.map(tag => <p className="Manga-tag" key={manga.internalId + "-tag-" + tag}><Icon
|
||||
path={mdiTagTextOutline} size={0.5}/> {tag}</p>)}
|
||||
{manga.authorIds.map(authorId =>
|
||||
<p className="Manga-author" key={manga.mangaId + "-author-" + authorId} >
|
||||
<Icon path={mdiAccountEdit} size={0.5} />
|
||||
<AuthorElement apiUri={apiUri} authorId={authorId}></AuthorElement>
|
||||
</p>)}
|
||||
{manga.tags.map(tag =>
|
||||
<p className="Manga-tag" key={manga.mangaId + "-tag-" + tag}>
|
||||
<Icon path={mdiTagTextOutline} size={0.5}/>
|
||||
{tag}
|
||||
</p>)}
|
||||
{manga.linkIds.map(linkId =>
|
||||
<p className="Manga-link" key={manga.mangaId + "-link-" + linkId}>
|
||||
<Icon path={mdiLinkVariant} size={0.5}/>
|
||||
<LinkElement apiUri={apiUri} linkId={linkId}></LinkElement>
|
||||
</p>)}
|
||||
</div>
|
||||
<MarkdownPreview className="Manga-description" source={manga.description}
|
||||
style={{backgroundColor: "transparent", color: "black"}}/>
|
||||
<button className="Manga-AddButton" onClick={() => {
|
||||
Job.CreateJobDateInterval(apiUri, manga.internalId, "MonitorManga", interval).then(() => onJobsChanged(manga.internalId));
|
||||
Job.CreateDownloadAvailableChaptersJob(apiUri, manga.mangaId, interval.getMilliseconds()).then(() => onJobsChanged(manga.mangaId));
|
||||
}}>Monitor
|
||||
</button>
|
||||
</div>);
|
||||
}
|
||||
|
||||
function ProgressbarStr(job: IJob): string {
|
||||
return job.progressToken.timeRemaining.substring(0,job.progressToken.timeRemaining.indexOf(".")).concat(" ", ToPercentString(job.progressToken.progress));
|
||||
}
|
||||
|
||||
function ToPercentString(n: number): string {
|
||||
return n.toString().substring(2,4).concat("%");
|
||||
}
|
||||
|
||||
export function QueueItem(apiUri: string, manga: IManga, job: IJob, triggerUpdate: () => void){
|
||||
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
|
||||
if(e.currentTarget.src != Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget))
|
||||
e.currentTarget.src = Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="QueueJob" key={"QueueJob-" + job.id}>
|
||||
<img src="../../media/blahaj.png" onLoad={MangaCover} alt="Manga Cover"></img>
|
||||
<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>
|
||||
{job.progressToken.state === 0
|
||||
? <ProgressBar labelColor={"#000"} height={"10px"} labelAlignment={"outside"}
|
||||
className="QueueJob-Progressbar" completed={job.progressToken.progress} maxCompleted={1}
|
||||
customLabel={ProgressbarStr(job)}/>
|
||||
: <div className="QueueJob-Progressbar"></div>}
|
||||
<div className="QueueJob" key={"QueueJob-" + job.jobId}>
|
||||
<img src="../../media/blahaj.png" alt="Manga Cover"></img>
|
||||
<p className="QueueJob-Name">{manga.name}</p>
|
||||
<p className="QueueJob-JobType">{job.jobType}</p>
|
||||
<div className="QueueJob-actions">
|
||||
<button className="QueueJob-Cancel"
|
||||
onClick={() => Job.CancelJob(apiUri, job.id).then(triggerUpdate)}>Cancel
|
||||
onClick={() => Job.StopJob(apiUri, job.jobId).then(triggerUpdate)}>Cancel
|
||||
</button>
|
||||
{job.parentJobId != null
|
||||
? <button className="QueueJob-Cancel"
|
||||
onClick={() => Job.CancelJob(apiUri, job.parentJobId!).then(triggerUpdate)}>Cancel all
|
||||
onClick={() => Job.StopJob(apiUri, job.parentJobId!).then(triggerUpdate)}>Cancel all
|
||||
related</button>
|
||||
: <></>
|
||||
}
|
||||
|
5
Website/modules/interfaces/IMangaAltTitle.ts
Normal file
5
Website/modules/interfaces/IMangaAltTitle.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default interface IMangaAltTitle {
|
||||
altTitleId: string;
|
||||
language: string;
|
||||
title: string;
|
||||
}
|
7
Website/modules/interfaces/IMangaConnector.ts
Normal file
7
Website/modules/interfaces/IMangaConnector.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default interface IMangaConnector {
|
||||
name: string;
|
||||
supportedLanguages: string[];
|
||||
iconUrl: string;
|
||||
baseUris: string[];
|
||||
enabled: boolean;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
export default interface IMangaConnector {
|
||||
SupportedLanguages: string[];
|
||||
name: string;
|
||||
BaseUris: string[];
|
||||
}
|
7
Website/modules/interfaces/INotificationConnector.ts
Normal file
7
Website/modules/interfaces/INotificationConnector.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default interface INotificationConnector {
|
||||
name: string;
|
||||
url: string;
|
||||
headers: Record<string, string>[];
|
||||
httpMethod: string;
|
||||
body: string;
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
export default interface IProgressToken{
|
||||
cancellationRequested: boolean;
|
||||
increments: number;
|
||||
incrementsCompleted: number;
|
||||
progress: number;
|
||||
lastUpdate: Date;
|
||||
executionStarted: Date;
|
||||
timeRemaining: string;
|
||||
state: number;
|
||||
}
|
||||
|
||||
export function GetProgressStateFromNumber(n: number): string {
|
||||
switch (n){
|
||||
case 0: return "Running";
|
||||
case 1: return "Complete";
|
||||
case 2: return "Standby";
|
||||
case 3: return "Cancelled";
|
||||
case 4: return "Waiting";
|
||||
}
|
||||
return "";
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export default interface KeyValuePair {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
export default interface ICoverFormatRequestRecord {
|
||||
size: Size;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
isEmpty: boolean;
|
||||
}
|
5
Website/modules/interfaces/records/IGotifyRecord.ts
Normal file
5
Website/modules/interfaces/records/IGotifyRecord.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default interface IGotifyRecord {
|
||||
endpoint: string;
|
||||
appToken: string;
|
||||
priority: number;
|
||||
}
|
4
Website/modules/interfaces/records/IModifyJobRecord.ts
Normal file
4
Website/modules/interfaces/records/IModifyJobRecord.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default interface IModifyJobRecord {
|
||||
recurrenceMs: number;
|
||||
enabled: boolean;
|
||||
}
|
7
Website/modules/interfaces/records/INtfyRecord.ts
Normal file
7
Website/modules/interfaces/records/INtfyRecord.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default interface INtfyRecord {
|
||||
endpoint: string;
|
||||
username: string;
|
||||
password: string;
|
||||
topic: string;
|
||||
priority: number;
|
||||
}
|
3
Website/modules/interfaces/records/IlunaseaRecord.ts
Normal file
3
Website/modules/interfaces/records/IlunaseaRecord.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default interface IlunaseaRecord {
|
||||
id: string;
|
||||
}
|
@ -76,6 +76,10 @@
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.SearchResult > .Manga-tags p > * {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.SearchResult .Manga-author {
|
||||
background-color: green;
|
||||
}
|
||||
@ -84,6 +88,14 @@
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
.SearchResult .Manga-link{
|
||||
background-color: brown;
|
||||
}
|
||||
|
||||
.SearchResult .Manga-link > a, .Manga-link > a:visited {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.SearchResult > .Manga-description {
|
||||
grid-area: description;
|
||||
color: black;
|
||||
|
@ -1,101 +0,0 @@
|
||||
#Settings {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#SettingsIcon {
|
||||
height: calc(100% - 26px);
|
||||
margin: 13px;
|
||||
filter: invert(100%) sepia(65%) saturate(2%) hue-rotate(215deg) brightness(113%) contrast(101%);
|
||||
}
|
||||
|
||||
#settingsPopupBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin: 5px 5px 10px;
|
||||
font-size: 10pt;
|
||||
font-weight: 100;
|
||||
display: block;
|
||||
border-top-style: solid;
|
||||
border-top-width: 1px;
|
||||
border-top-color: lightgray;
|
||||
width: calc(100% - 30px);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.settings-section-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
|
||||
}
|
||||
|
||||
.section-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 22%;
|
||||
min-width: 300px;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: lightgray;
|
||||
margin: 7px;
|
||||
padding: 5px 5px 35px;
|
||||
}
|
||||
|
||||
.section-item > * {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.section-item[connector-status="Not Configured"]{
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.section-item[connector-status="Configured"]{
|
||||
border-color: green;
|
||||
}
|
||||
|
||||
.section-item > .settings-section-title {
|
||||
font-weight: bold;
|
||||
vertical-align: bottom;
|
||||
line-height: 32px;
|
||||
font-size: 12pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-item > .settings-section-title > img {
|
||||
width: auto;
|
||||
height: 32px;
|
||||
margin: 5px;
|
||||
vertical-align: middle;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.section-item .section-actions {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: 5px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.section-actions > span, #resetUserAgent {
|
||||
border: 1px solid lightgray;
|
||||
padding: 3px 5px 2px;
|
||||
width: 10ch;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#resetUserAgent{
|
||||
align-self: flex-end;
|
||||
margin-top: 3px;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user