From 84520d8e18fecf88da20e2fd46858654e18682ce Mon Sep 17 00:00:00 2001 From: glax Date: Thu, 13 Mar 2025 20:08:37 +0100 Subject: [PATCH] Adjust all endpoints and methods to tranga/postgres-Server-V2. Search working. --- Website/App.tsx | 62 +++- Website/modules/Footer.tsx | 13 +- Website/modules/Header.tsx | 2 - Website/modules/Job.tsx | 235 ++++++++----- Website/modules/Manga.tsx | 159 +++++++-- Website/modules/MangaConnector.tsx | 39 ++- Website/modules/MonitorJobsList.tsx | 39 +-- Website/modules/NotificationConnector.tsx | 218 +++++------- Website/modules/QueuePopUp.tsx | 69 +--- Website/modules/Search.tsx | 19 +- Website/modules/SearchFunctions.tsx | 46 +++ Website/modules/Settings.tsx | 311 ------------------ Website/modules/interfaces/IAuthor.tsx | 21 ++ ...ackendSettings.tsx => IBackendSettings.ts} | 5 - Website/modules/interfaces/IChapter.ts | 10 + Website/modules/interfaces/IChapter.tsx | 10 - Website/modules/interfaces/IJob.ts | 28 ++ Website/modules/interfaces/IJob.tsx | 28 -- .../modules/interfaces/ILibraryConnector.ts | 11 + .../modules/interfaces/ILibraryConnector.tsx | 13 - Website/modules/interfaces/ILink.tsx | 25 ++ Website/modules/interfaces/IManga.tsx | 144 ++++---- Website/modules/interfaces/IMangaAltTitle.ts | 5 + Website/modules/interfaces/IMangaConnector.ts | 7 + .../modules/interfaces/IMangaConnector.tsx | 5 - .../interfaces/INotificationConnector.ts | 7 + .../interfaces/INotificationConnector.tsx | 8 - Website/modules/interfaces/IProgressToken.tsx | 21 -- Website/modules/interfaces/KeyValuePair.tsx | 4 - .../records/ICoverFormatRequestRecord.ts | 9 + .../interfaces/records/IGotifyRecord.ts | 5 + .../interfaces/records/IModifyJobRecord.ts | 4 + .../modules/interfaces/records/INtfyRecord.ts | 7 + .../interfaces/records/IlunaseaRecord.ts | 3 + Website/styles/MangaSearchResult.css | 12 + Website/styles/settings.css | 101 ------ 36 files changed, 729 insertions(+), 976 deletions(-) create mode 100644 Website/modules/SearchFunctions.tsx create mode 100644 Website/modules/interfaces/IAuthor.tsx rename Website/modules/interfaces/{IBackendSettings.tsx => IBackendSettings.ts} (71%) create mode 100644 Website/modules/interfaces/IChapter.ts delete mode 100644 Website/modules/interfaces/IChapter.tsx create mode 100644 Website/modules/interfaces/IJob.ts delete mode 100644 Website/modules/interfaces/IJob.tsx create mode 100644 Website/modules/interfaces/ILibraryConnector.ts delete mode 100644 Website/modules/interfaces/ILibraryConnector.tsx create mode 100644 Website/modules/interfaces/ILink.tsx create mode 100644 Website/modules/interfaces/IMangaAltTitle.ts create mode 100644 Website/modules/interfaces/IMangaConnector.ts delete mode 100644 Website/modules/interfaces/IMangaConnector.tsx create mode 100644 Website/modules/interfaces/INotificationConnector.ts delete mode 100644 Website/modules/interfaces/INotificationConnector.tsx delete mode 100644 Website/modules/interfaces/IProgressToken.tsx delete mode 100644 Website/modules/interfaces/KeyValuePair.tsx create mode 100644 Website/modules/interfaces/records/ICoverFormatRequestRecord.ts create mode 100644 Website/modules/interfaces/records/IGotifyRecord.ts create mode 100644 Website/modules/interfaces/records/IModifyJobRecord.ts create mode 100644 Website/modules/interfaces/records/INtfyRecord.ts create mode 100644 Website/modules/interfaces/records/IlunaseaRecord.ts diff --git a/Website/App.tsx b/Website/App.tsx index 1788095..a607533 100644 --- a/Website/App.tsx +++ b/Website/App.tsx @@ -14,6 +14,7 @@ export default function App(){ const [frontendSettings, setFrontendSettings] = useState(LoadFrontendSettings()); const [updateInterval, setUpdateInterval] = React.useState(); const [updateMonitorList, setUpdateMonitorList] = React.useState(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 { }); } -export function postData(uri: string, content: object) : Promise { +export function postData(uri: string, content: object | string | number) : Promise { return fetch(uri, { method: 'POST', @@ -116,6 +117,50 @@ export function deleteData(uri: string) : Promise { }); } +export function patchData(uri: string, content: object | string | number) : Promise { + 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 { + 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 =>{ - 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(); + }); } \ No newline at end of file diff --git a/Website/modules/Footer.tsx b/Website/modules/Footer.tsx index d0d9a44..13e451c 100644 --- a/Website/modules/Footer.tsx +++ b/Website/modules/Footer.tsx @@ -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(); 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
{RunningJobsCount}
+ -
{StandbyJobsCount}
+
{WaitingJobsCount}
=
{AllJobsCount}
diff --git a/Website/modules/Header.tsx b/Website/modules/Header.tsx index bdd76ea..f579062 100644 --- a/Website/modules/Header.tsx +++ b/Website/modules/Header.tsx @@ -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 website image is Blahaj Tranga - ) } \ No newline at end of file diff --git a/Website/modules/Job.tsx b/Website/modules/Job.tsx index 09709b7..9aa9da0 100644 --- a/Website/modules/Job.tsx +++ b/Website/modules/Job.tsx @@ -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 { + static async GetAllJobs(apiUri: string): Promise { //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 { - //console.info("Getting all running Jobs"); - return getData(`${apiUri}/v2/Jobs/Running`) + static async GetJobsWithIds(apiUri: string, jobIds: string[]): Promise { + 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 { + 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 { - //console.info("Getting all waiting Jobs"); - return getData(`${apiUri}/v2/Jobs/Waiting`) + static async GetJobsWithType(apiUri: string, jobType: JobType): Promise { + 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 { - //console.info("Getting all standby Jobs"); - return getData(`${apiUri}/v2/Jobs/Standby`) + static async GetJobsOfTypeAndWithState(apiUri: string, jobType: JobType, state: JobState): Promise { + 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 { - //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 { - 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 { - //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 { - return this.CreateJob(apiUri, internalId, jobType, this.IntervalStringFromDate(interval)); - } - - static async CreateJob(apiUri: string, internalId: string, jobType: string, interval: string): Promise { - 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 { + 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 { - return postData(`${apiUri}/v2/Job/${jobId}/StartNow`, {}); + static async ModifyJob(apiUri: string, jobId: string, modifyData: IModifyJobRecord): Promise { + 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 { - return postData(`${apiUri}/v2/Job/${jobId}/Cancel`, {}); + static async CreateDownloadAvailableChaptersJob(apiUri: string, mangaId: string, recurrenceMs: number): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return postData(`${apiUri}/v2/Job/${jobId}/Start`, {}); + } + + static StopJob(apiUri: string, jobId: string) : Promise { + return postData(`${apiUri}/v2/Job/${jobId}/Stop`, {}); } } \ No newline at end of file diff --git a/Website/modules/Manga.tsx b/Website/modules/Manga.tsx index 2e0e419..bde6c16 100644 --- a/Website/modules/Manga.tsx +++ b/Website/modules/Manga.tsx @@ -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 { //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 { - //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 { - //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 { + static async GetMangaWithIds(apiUri: string, mangaIds: string[]): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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}); } } \ No newline at end of file diff --git a/Website/modules/MangaConnector.tsx b/Website/modules/MangaConnector.tsx index 25637f0..a515645 100644 --- a/Website/modules/MangaConnector.tsx +++ b/Website/modules/MangaConnector.tsx @@ -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 { + static async GetAllConnectors(apiUri: string): Promise { //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 { - //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 { + //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 { - //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 { + //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 { + 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}`, {}); + } } \ No newline at end of file diff --git a/Website/modules/MonitorJobsList.tsx b/Website/modules/MonitorJobsList.tsx index 2d1c11b..1237ff2 100644 --- a/Website/modules/MonitorJobsList.tsx +++ b/Website/modules/MonitorJobsList.tsx @@ -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, connectedToBackend: boolean, apiUri: string, updateList: Date}) { const [MonitoringJobs, setMonitoringJobs] = useState([]); @@ -14,18 +11,7 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged, connected const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState(); 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 (
{StartSearchMangaEntry()} - {AllManga.map((manga: IManga) => { - const job = MonitoringJobs.find(job => job.mangaInternalId == manga.internalId); - if (job === undefined || job == null) - return
Error. Could not find matching job for {manga.internalId}
- return
- {CoverCard(apiUri, manga)} -
-
-
-
-
; - })}
) } \ No newline at end of file diff --git a/Website/modules/NotificationConnector.tsx b/Website/modules/NotificationConnector.tsx index 6223653..f9d606e 100644 --- a/Website/modules/NotificationConnector.tsx +++ b/Website/modules/NotificationConnector.tsx @@ -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 { //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; - public abstract Reset(apiUri: string) : Promise; - public abstract Create(apiUri: string) : Promise; - protected abstract CheckConnector() : boolean; - - protected async TestConnector(apiUri: string, connectorType: string, data: object): Promise { - 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 { + 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 { - 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 { - 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 { - return this.TestConnector(apiUri, "Gotify", {url: this.url, appToken: this.appToken}).then(() => true).catch(() => false); - } - - public async Reset(apiUri: string) : Promise { - return this.ResetConnector(apiUri, "Gotify").then(() => true).catch(() => false); - } - - public async Create(apiUri: string) : Promise { - 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 { + 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 { - return this.TestConnector(apiUri, "LunaSea", {webhook: this.webhook}).then(() => true).catch(() => false); - } - - public async Reset(apiUri: string) : Promise { - return this.ResetConnector(apiUri, "LunaSea").then(() => true).catch(() => false); - } - - public async Create(apiUri: string) : Promise { - 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 { - 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 { - return this.ResetConnector(apiUri, "Ntfy").then(() => true).catch(() => false); - } - - public async Create(apiUri: string) : Promise { - 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 { + 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 { + 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 { + 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 { + 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); + }); } } \ No newline at end of file diff --git a/Website/modules/QueuePopUp.tsx b/Website/modules/QueuePopUp.tsx index f9e4b57..d2ee1d6 100644 --- a/Website/modules/QueuePopUp.tsx +++ b/Website/modules/QueuePopUp.tsx @@ -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([]); - const [StandbyJobsManga, setStandbyJobsManga] = React.useState([]); + const [WaitingJobs, setWaitingJobs] = React.useState([]); const [RunningJobs, setRunningJobs] = React.useState([]); - const [RunningJobsManga, setRunningJobsManga] = React.useState([]); const [showQueuePopup, setShowQueuePopup] = useState(false); const [queueListInterval, setQueueListInterval] = React.useState(); @@ -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 (<>
setShowQueuePopup(true)}> {children} @@ -89,28 +58,6 @@ export default function QueuePopUp({connectedToBackend, children, apiUri} : {con onClick={() => setShowQueuePopup(false)}/>
-
-

Running

-
- {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
Error. Could not find matching manga for {job.id}
- return QueueItem(apiUri, manga, job, UpdateMonitoringJobsList); - })} -
-
-
-

Standby

-
- {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
Error. Could not find matching manga for {job.id}
- return QueueItem(apiUri, manga, job, UpdateMonitoringJobsList); - })} -
-
: <> diff --git a/Website/modules/Search.tsx b/Website/modules/Search.tsx index f0d1c0a..23dfdd2 100644 --- a/Website/modules/Search.tsx +++ b/Website/modules/Search.tsx @@ -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(); @@ -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 = (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} @@ -107,5 +108,5 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch} ?

: searchResults.map(result => SearchResult(apiUri, result, jobInterval, onJobsChanged))} - ) + ); } \ No newline at end of file diff --git a/Website/modules/SearchFunctions.tsx b/Website/modules/SearchFunctions.tsx new file mode 100644 index 0000000..97260e4 --- /dev/null +++ b/Website/modules/SearchFunctions.tsx @@ -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 { + 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 { + 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 { + 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); + }); + } + +} \ No newline at end of file diff --git a/Website/modules/Settings.tsx b/Website/modules/Settings.tsx index 6c16302..75df758 100644 --- a/Website/modules/Settings.tsx +++ b/Website/modules/Settings.tsx @@ -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(settings); - const [backendSettings, setBackendSettings] = useState(); - const [showSettings, setShowSettings] = useState(false); - const [libraryConnectors, setLibraryConnectors] = useState(); - const [notificationConnectors, setNotificationConnectors] = useState(); - 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 { - //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 = (e) => { - if(e.currentTarget.value.length < 1) - return; - if(e.key == "Enter"){ - setFrontendSettings({...frontendSettings, apiUri: e.currentTarget.value}); - RefreshInputs(); - } - } - - const SubmitUserAgent: KeyboardEventHandler = (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 = () => { - //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 = (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 = (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 = (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 ( -
- cogwheel setShowSettings(true)}/> - {showSettings - ?
-
-

Settings

- Close Settings setShowSettings(false)}/> -
-
-
- TRANGA -
-
- API Settings - - - - - Reset - - - - - - -
-
- Rate Limits - - - - - - - - -
-
- Appearance - - -
-
-
- -
- Sources -
-
- - Mangadex Logo - MangaDex - - - - - -
-
-
- -
- LIBRARY CONNECTORS -
-
- - Komga Logo - Komga - - - setKomgaSettings(s => ({...s, url: e.target.value}))} /> - - setKomgaSettings(s => ({...s, username: e.target.value}))} /> - - setKomgaSettings(s => ({...s, password: e.target.value}))} /> -
- new Komga(komgaSettings).Test(apiUri).then(()=>alert("Test successful"))}>Test - new Komga(komgaSettings).Reset(apiUri).then(RefreshInputs)}>Reset - new Komga(komgaSettings).Create(apiUri).then(RefreshInputs)}>Apply -
-
-
- - Kavita Logo - Kavita - - - setKavitaSettings(s => ({...s, url: e.target.value}))} /> - - setKavitaSettings(s => ({...s, username: e.target.value}))} /> - - setKavitaSettings(s => ({...s, password: e.target.value}))} /> -
- new Kavita(kavitaSettings).Test(apiUri).then(()=>alert("Test successful"))}>Test - new Kavita(kavitaSettings).Reset(apiUri).then(RefreshInputs)}>Reset - new Kavita(kavitaSettings).Create(apiUri).then(RefreshInputs)}>Apply -
-
-
-
- -
- NOTIFICATION CONNECTORS -
-
- - Gotify Logo - Gotify - - - setGotifySettings(s => ({...s, url: e.target.value}))} /> - - setGotifySettings(s => ({...s, appToken: e.target.value}))} /> -
- new Gotify(gotifySettings).Test(apiUri).then(()=>alert("Test successful"))}>Test - new Gotify(gotifySettings).Reset(apiUri).then(RefreshInputs)}>Reset - new Gotify(gotifySettings).Create(apiUri).then(RefreshInputs)}>Apply -
-
-
- - Lunasea Logo - LunaSea - - - setLunaseaSettings(s => ({...s, webhook: e.target.value}))} /> -
- new Lunasea(lunaseaSettings).Test(apiUri).then(()=>alert("Test successful"))}>Test - new Lunasea(lunaseaSettings).Reset(apiUri).then(RefreshInputs)}>Reset - new Lunasea(lunaseaSettings).Create(apiUri).then(RefreshInputs)}>Apply -
-
-
- - ntfy Logo - Ntfy - - - setNtfySettings(s => ({...s, url: e.target.value}))} /> - - setNtfySettings(s => ({...s, username: e.target.value}))} /> - - setNtfySettings(s => ({...s, password: e.target.value}))} /> - - setNtfySettings(s => ({...s, topic: e.target.value}))} /> -
- new Ntfy(ntfySettings).Test(apiUri).then(()=>alert("Test successful"))}>Test - new Ntfy(ntfySettings).Reset(apiUri).then(RefreshInputs)}>Reset - new Ntfy(ntfySettings).Create(apiUri).then(RefreshInputs)}>Apply -
-
-
-
-
-
- : <> - } -
- ); } \ No newline at end of file diff --git a/Website/modules/interfaces/IAuthor.tsx b/Website/modules/interfaces/IAuthor.tsx new file mode 100644 index 0000000..1db7701 --- /dev/null +++ b/Website/modules/interfaces/IAuthor.tsx @@ -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(authorId); + + useEffect(()=> { + getData(`${apiUri}/v2/Query/Author/${authorId}`) + .then((json) => { + let ret = json as IAuthor; + setName(ret.authorName); + }); + }, []) + + return ({name}); +} \ No newline at end of file diff --git a/Website/modules/interfaces/IBackendSettings.tsx b/Website/modules/interfaces/IBackendSettings.ts similarity index 71% rename from Website/modules/interfaces/IBackendSettings.tsx rename to Website/modules/interfaces/IBackendSettings.ts index a4edf2a..5661cff 100644 --- a/Website/modules/interfaces/IBackendSettings.tsx +++ b/Website/modules/interfaces/IBackendSettings.ts @@ -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; diff --git a/Website/modules/interfaces/IChapter.ts b/Website/modules/interfaces/IChapter.ts new file mode 100644 index 0000000..e255ff6 --- /dev/null +++ b/Website/modules/interfaces/IChapter.ts @@ -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; +} \ No newline at end of file diff --git a/Website/modules/interfaces/IChapter.tsx b/Website/modules/interfaces/IChapter.tsx deleted file mode 100644 index 5d4eb41..0000000 --- a/Website/modules/interfaces/IChapter.tsx +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/Website/modules/interfaces/IJob.ts b/Website/modules/interfaces/IJob.ts new file mode 100644 index 0000000..a9644d6 --- /dev/null +++ b/Website/modules/interfaces/IJob.ts @@ -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" +} \ No newline at end of file diff --git a/Website/modules/interfaces/IJob.tsx b/Website/modules/interfaces/IJob.tsx deleted file mode 100644 index 85baf23..0000000 --- a/Website/modules/interfaces/IJob.tsx +++ /dev/null @@ -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 ""; -} \ No newline at end of file diff --git a/Website/modules/interfaces/ILibraryConnector.ts b/Website/modules/interfaces/ILibraryConnector.ts new file mode 100644 index 0000000..a477475 --- /dev/null +++ b/Website/modules/interfaces/ILibraryConnector.ts @@ -0,0 +1,11 @@ +export default interface ILibraryConnector { + libraryConnectorId: string; + libraryType: LibraryType; + baseUrl: string; + auth: string; +} + +export enum LibraryType { + Komga = "Komga", + Kavita = "Kavita" +} \ No newline at end of file diff --git a/Website/modules/interfaces/ILibraryConnector.tsx b/Website/modules/interfaces/ILibraryConnector.tsx deleted file mode 100644 index c1520c6..0000000 --- a/Website/modules/interfaces/ILibraryConnector.tsx +++ /dev/null @@ -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 ""; -} \ No newline at end of file diff --git a/Website/modules/interfaces/ILink.tsx b/Website/modules/interfaces/ILink.tsx new file mode 100644 index 0000000..3019dc3 --- /dev/null +++ b/Website/modules/interfaces/ILink.tsx @@ -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(linkId); + let [linkUrl, setLinkUrl] = React.useState(""); + + useEffect(()=> { + getData(`${apiUri}/v2/Query/Link/${linkId}`) + .then((json) => { + let ret = json as ILink; + setProvider(ret.linkProvider); + setLinkUrl(ret.linkUrl); + }); + }, []) + + return ({provider}); +} \ No newline at end of file diff --git a/Website/modules/interfaces/IManga.tsx b/Website/modules/interfaces/IManga.tsx index eba776c..0af58a4 100644 --- a/Website/modules/interfaces/IManga.tsx +++ b/Website/modules/interfaces/IManga.tsx @@ -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 = (e) => { - if(e.currentTarget.src != Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget)) - e.currentTarget.src = Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget); - } - return( -
- Manga Cover +
+ Manga Cover
-

{manga.mangaConnector.name}

-
-

{manga.sortName}

+

{manga.mangaConnectorId}

+
+

{manga.name}

); } export function SearchResult(apiUri: string, manga: IManga, interval: Date, onJobsChanged: (internalId: string) => void) : ReactElement { const MangaCover : ReactEventHandler = (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( -
- Manga Cover -

{manga.mangaConnector.name}

-
-

{manga.sortName} + Manga Cover +

{manga.mangaConnectorId}

+
+

{manga.name}

- {manga.authors.map(author =>

- {author}

)} - {manga.tags.map(tag =>

{tag}

)} + {manga.authorIds.map(authorId => +

+ + +

)} + {manga.tags.map(tag => +

+ + {tag} +

)} + {manga.linkIds.map(linkId => +

+ + +

)}
); } -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 = (e) => { - if(e.currentTarget.src != Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget)) - e.currentTarget.src = Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget); - } - return ( -
- Manga Cover -

{manga.sortName}

-

{JobTypeFromNumber(job.jobType)}

-

{job.jobType == 0 ? `Vol.${job.chapter?.volumeNumber} Ch.${job.chapter?.chapterNumber}` : ""}

- {job.progressToken.state === 0 - ? - :
} +
+ Manga Cover +

{manga.name}

+

{job.jobType}

{job.parentJobId != null ? : <> } diff --git a/Website/modules/interfaces/IMangaAltTitle.ts b/Website/modules/interfaces/IMangaAltTitle.ts new file mode 100644 index 0000000..2526df4 --- /dev/null +++ b/Website/modules/interfaces/IMangaAltTitle.ts @@ -0,0 +1,5 @@ +export default interface IMangaAltTitle { + altTitleId: string; + language: string; + title: string; +} \ No newline at end of file diff --git a/Website/modules/interfaces/IMangaConnector.ts b/Website/modules/interfaces/IMangaConnector.ts new file mode 100644 index 0000000..61fb457 --- /dev/null +++ b/Website/modules/interfaces/IMangaConnector.ts @@ -0,0 +1,7 @@ +export default interface IMangaConnector { + name: string; + supportedLanguages: string[]; + iconUrl: string; + baseUris: string[]; + enabled: boolean; +} \ No newline at end of file diff --git a/Website/modules/interfaces/IMangaConnector.tsx b/Website/modules/interfaces/IMangaConnector.tsx deleted file mode 100644 index 94fd237..0000000 --- a/Website/modules/interfaces/IMangaConnector.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export default interface IMangaConnector { - SupportedLanguages: string[]; - name: string; - BaseUris: string[]; -} \ No newline at end of file diff --git a/Website/modules/interfaces/INotificationConnector.ts b/Website/modules/interfaces/INotificationConnector.ts new file mode 100644 index 0000000..8d2b765 --- /dev/null +++ b/Website/modules/interfaces/INotificationConnector.ts @@ -0,0 +1,7 @@ +export default interface INotificationConnector { + name: string; + url: string; + headers: Record[]; + httpMethod: string; + body: string; +} \ No newline at end of file diff --git a/Website/modules/interfaces/INotificationConnector.tsx b/Website/modules/interfaces/INotificationConnector.tsx deleted file mode 100644 index a3ff39d..0000000 --- a/Website/modules/interfaces/INotificationConnector.tsx +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/Website/modules/interfaces/IProgressToken.tsx b/Website/modules/interfaces/IProgressToken.tsx deleted file mode 100644 index f33a507..0000000 --- a/Website/modules/interfaces/IProgressToken.tsx +++ /dev/null @@ -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 ""; -} \ No newline at end of file diff --git a/Website/modules/interfaces/KeyValuePair.tsx b/Website/modules/interfaces/KeyValuePair.tsx deleted file mode 100644 index 165ba57..0000000 --- a/Website/modules/interfaces/KeyValuePair.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export default interface KeyValuePair { - key: string; - value: string; -} \ No newline at end of file diff --git a/Website/modules/interfaces/records/ICoverFormatRequestRecord.ts b/Website/modules/interfaces/records/ICoverFormatRequestRecord.ts new file mode 100644 index 0000000..ea4a565 --- /dev/null +++ b/Website/modules/interfaces/records/ICoverFormatRequestRecord.ts @@ -0,0 +1,9 @@ +export default interface ICoverFormatRequestRecord { + size: Size; +} + +export interface Size { + width: number; + height: number; + isEmpty: boolean; +} \ No newline at end of file diff --git a/Website/modules/interfaces/records/IGotifyRecord.ts b/Website/modules/interfaces/records/IGotifyRecord.ts new file mode 100644 index 0000000..ffd66ae --- /dev/null +++ b/Website/modules/interfaces/records/IGotifyRecord.ts @@ -0,0 +1,5 @@ +export default interface IGotifyRecord { + endpoint: string; + appToken: string; + priority: number; +} \ No newline at end of file diff --git a/Website/modules/interfaces/records/IModifyJobRecord.ts b/Website/modules/interfaces/records/IModifyJobRecord.ts new file mode 100644 index 0000000..3241c9a --- /dev/null +++ b/Website/modules/interfaces/records/IModifyJobRecord.ts @@ -0,0 +1,4 @@ +export default interface IModifyJobRecord { + recurrenceMs: number; + enabled: boolean; +} \ No newline at end of file diff --git a/Website/modules/interfaces/records/INtfyRecord.ts b/Website/modules/interfaces/records/INtfyRecord.ts new file mode 100644 index 0000000..9c67e0c --- /dev/null +++ b/Website/modules/interfaces/records/INtfyRecord.ts @@ -0,0 +1,7 @@ +export default interface INtfyRecord { + endpoint: string; + username: string; + password: string; + topic: string; + priority: number; +} \ No newline at end of file diff --git a/Website/modules/interfaces/records/IlunaseaRecord.ts b/Website/modules/interfaces/records/IlunaseaRecord.ts new file mode 100644 index 0000000..85a0200 --- /dev/null +++ b/Website/modules/interfaces/records/IlunaseaRecord.ts @@ -0,0 +1,3 @@ +export default interface IlunaseaRecord { + id: string; +} \ No newline at end of file diff --git a/Website/styles/MangaSearchResult.css b/Website/styles/MangaSearchResult.css index 3b22ddd..727b642 100644 --- a/Website/styles/MangaSearchResult.css +++ b/Website/styles/MangaSearchResult.css @@ -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; diff --git a/Website/styles/settings.css b/Website/styles/settings.css index 85a515f..e69de29 100644 --- a/Website/styles/settings.css +++ b/Website/styles/settings.css @@ -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; -} \ No newline at end of file