Adjust all endpoints and methods to tranga/postgres-Server-V2.

Search working.
This commit is contained in:
glax 2025-03-13 20:08:37 +01:00
parent 0402f9e6d0
commit 84520d8e18
36 changed files with 729 additions and 976 deletions

View File

@ -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();
});
}

View File

@ -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>

View File

@ -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>)
}

View File

@ -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`, {});
}
}

View File

@ -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});
}
}

View File

@ -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}`, {});
}
}

View File

@ -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>)
}

View File

@ -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);
});
}
}

View File

@ -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>
: <></>

View File

@ -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>);
}

View 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);
});
}
}

View File

@ -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>
);
}

View 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>);
}

View File

@ -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;

View 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;
}

View File

@ -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;
}

View 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"
}

View File

@ -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 "";
}

View File

@ -0,0 +1,11 @@
export default interface ILibraryConnector {
libraryConnectorId: string;
libraryType: LibraryType;
baseUrl: string;
auth: string;
}
export enum LibraryType {
Komga = "Komga",
Kavita = "Kavita"
}

View File

@ -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 "";
}

View 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>);
}

View File

@ -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>
: <></>
}

View File

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

View File

@ -0,0 +1,7 @@
export default interface IMangaConnector {
name: string;
supportedLanguages: string[];
iconUrl: string;
baseUris: string[];
enabled: boolean;
}

View File

@ -1,5 +0,0 @@
export default interface IMangaConnector {
SupportedLanguages: string[];
name: string;
BaseUris: string[];
}

View File

@ -0,0 +1,7 @@
export default interface INotificationConnector {
name: string;
url: string;
headers: Record<string, string>[];
httpMethod: string;
body: string;
}

View File

@ -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
}

View File

@ -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 "";
}

View File

@ -1,4 +0,0 @@
export default interface KeyValuePair {
key: string;
value: string;
}

View File

@ -0,0 +1,9 @@
export default interface ICoverFormatRequestRecord {
size: Size;
}
export interface Size {
width: number;
height: number;
isEmpty: boolean;
}

View File

@ -0,0 +1,5 @@
export default interface IGotifyRecord {
endpoint: string;
appToken: string;
priority: number;
}

View File

@ -0,0 +1,4 @@
export default interface IModifyJobRecord {
recurrenceMs: number;
enabled: boolean;
}

View File

@ -0,0 +1,7 @@
export default interface INtfyRecord {
endpoint: string;
username: string;
password: string;
topic: string;
priority: number;
}

View File

@ -0,0 +1,3 @@
export default interface IlunaseaRecord {
id: string;
}

View File

@ -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;

View File

@ -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;
}