Update to mui
156
Website/App.tsx
@ -1,156 +0,0 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
|
||||||
import Footer from "./modules/Footer";
|
|
||||||
import Search from "./modules/Search";
|
|
||||||
import Header from "./modules/Header";
|
|
||||||
import MonitorJobsList from "./modules/MonitorJobsList";
|
|
||||||
import './styles/index.css'
|
|
||||||
import {useCookies} from "react-cookie";
|
|
||||||
import Loader from "./modules/Loader";
|
|
||||||
import IFrontendSettings from "./modules/types/IFrontendSettings";
|
|
||||||
import {LoadFrontendSettings} from "./modules/api/FrontendSettings";
|
|
||||||
|
|
||||||
export default function App(){
|
|
||||||
const [, setCookie] = useCookies(['apiUri', 'jobInterval']);
|
|
||||||
const [connected, setConnected] = React.useState(false);
|
|
||||||
const [showSearch, setShowSearch] = React.useState(false);
|
|
||||||
const [frontendSettings, setFrontendSettings] = useState<IFrontendSettings>(LoadFrontendSettings());
|
|
||||||
const [updateInterval, setUpdateInterval] = React.useState<number | undefined>(undefined);
|
|
||||||
const checkConnectedInterval = 1000;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCookie('apiUri', frontendSettings.apiUri);
|
|
||||||
setCookie('jobInterval', frontendSettings.jobInterval);
|
|
||||||
updateConnected(frontendSettings.apiUri, connected, setConnected);
|
|
||||||
}, [frontendSettings]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(updateInterval === undefined){
|
|
||||||
setUpdateInterval(setInterval(() => {
|
|
||||||
updateConnected(frontendSettings.apiUri, connected, setConnected);
|
|
||||||
}, checkConnectedInterval));
|
|
||||||
}else{
|
|
||||||
clearInterval(updateInterval);
|
|
||||||
setUpdateInterval(undefined);
|
|
||||||
}
|
|
||||||
}, [connected]);
|
|
||||||
|
|
||||||
return(<div>
|
|
||||||
<Header apiUri={frontendSettings.apiUri} backendConnected={connected} settings={frontendSettings} setFrontendSettings={setFrontendSettings} />
|
|
||||||
{connected
|
|
||||||
? <>
|
|
||||||
{showSearch
|
|
||||||
? <>
|
|
||||||
<Search apiUri={frontendSettings.apiUri} jobInterval={frontendSettings.jobInterval} closeSearch={() => setShowSearch(false)} />
|
|
||||||
<hr/>
|
|
||||||
</>
|
|
||||||
: <></>}
|
|
||||||
<MonitorJobsList apiUri={frontendSettings.apiUri} onStartSearch={() => setShowSearch(true)} connectedToBackend={connected} checkConnectedInterval={checkConnectedInterval} />
|
|
||||||
</>
|
|
||||||
: <>
|
|
||||||
<h1>No connection to the Backend.</h1>
|
|
||||||
<h3>Check the Settings ApiUri.</h3>
|
|
||||||
<Loader loading={true} />
|
|
||||||
</>}
|
|
||||||
<Footer apiUri={frontendSettings.apiUri} connectedToBackend={connected} checkConnectedInterval={checkConnectedInterval} />
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getData(uri: string) : Promise<object> {
|
|
||||||
return makeRequest("GET", uri, null) as Promise<object>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function postData(uri: string, content: object | string | number | boolean) : Promise<object> {
|
|
||||||
return makeRequest("POST", uri, content) as Promise<object>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteData(uri: string) : Promise<void> {
|
|
||||||
return makeRequest("DELETE", uri, null) as Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function patchData(uri: string, content: object | string | number | boolean) : Promise<object> {
|
|
||||||
return makeRequest("patch", uri, content) as Promise<object>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function putData(uri: string, content: object | string | number | boolean) : Promise<object> {
|
|
||||||
return makeRequest("PUT", uri, content) as Promise<object>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentlyRequestedEndpoints: string[] = [];
|
|
||||||
function makeRequest(method: string, uri: string, content: object | string | number | null | boolean) : Promise<object | void> {
|
|
||||||
const id = method + uri;
|
|
||||||
if(currentlyRequestedEndpoints.find(x => x == id) != undefined)
|
|
||||||
return Promise.reject(`Already requested: ${method} ${uri}`);
|
|
||||||
currentlyRequestedEndpoints.push(id);
|
|
||||||
return fetch(uri,
|
|
||||||
{
|
|
||||||
method: method,
|
|
||||||
headers : {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
body: content ? JSON.stringify(content) : null
|
|
||||||
})
|
|
||||||
.then(function(response){
|
|
||||||
if(!response.ok){
|
|
||||||
if(response.status === 503){
|
|
||||||
let retryHeaderVal = response.headers.get("Retry-After");
|
|
||||||
let seconds = 10;
|
|
||||||
if(!retryHeaderVal){
|
|
||||||
return response.text().then(text => {
|
|
||||||
seconds = parseInt(text);
|
|
||||||
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
|
||||||
.then(() => {
|
|
||||||
return makeRequest(method, uri, content);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}else {
|
|
||||||
seconds = parseInt(retryHeaderVal);
|
|
||||||
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
|
||||||
.then(() => {
|
|
||||||
return makeRequest(method, uri, content);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}else
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
}
|
|
||||||
let json = response.json();
|
|
||||||
return json.then((json) => json).catch(() => null);
|
|
||||||
})
|
|
||||||
.catch(function(err : Error){
|
|
||||||
console.error(`Error ${method}ing Data ${uri}\n${err}`);
|
|
||||||
return Promise.reject();
|
|
||||||
}).finally(() => currentlyRequestedEndpoints.splice(currentlyRequestedEndpoints.indexOf(id), 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidUri(uri: string) : boolean{
|
|
||||||
try {
|
|
||||||
new URL(uri);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateConnected = (apiUri: string, connected: boolean, setConnected: (c: boolean) => void) => {
|
|
||||||
checkConnection(apiUri)
|
|
||||||
.then(res => {
|
|
||||||
if(connected != res)
|
|
||||||
setConnected(res);
|
|
||||||
})
|
|
||||||
.catch(() => setConnected(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
|
||||||
return fetch(`${apiUri}/swagger/v2/swagger.json`,
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if(!(response.ok && response.status == 200))
|
|
||||||
return false;
|
|
||||||
return response.json().then((json) => (json as {openapi:string}).openapi.match("[0-9]+(?:\.[0-9]+)+")?true:false).catch(() => false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return Promise.reject();
|
|
||||||
});
|
|
||||||
}
|
|
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 440 B |
@ -1,113 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
height="512pt"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
width="512pt"
|
|
||||||
version="1.1"
|
|
||||||
id="svg4586"
|
|
||||||
sodipodi:docname="komga - Copy.svg"
|
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
|
||||||
<metadata
|
|
||||||
id="metadata4592">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs4590">
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient6082"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#000000;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop6080"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient6076"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#000000;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop6074"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient6082"
|
|
||||||
id="linearGradient6084"
|
|
||||||
x1="77.866812"
|
|
||||||
y1="386.00679"
|
|
||||||
x2="217.20259"
|
|
||||||
y2="386.00679"
|
|
||||||
gradientUnits="userSpaceOnUse"/>
|
|
||||||
</defs>
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1656"
|
|
||||||
inkscape:window-height="1368"
|
|
||||||
id="namedview4588"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="1.2512475"
|
|
||||||
inkscape:cx="264.73114"
|
|
||||||
inkscape:cy="305.20589"
|
|
||||||
inkscape:window-x="-7"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg4586"/>
|
|
||||||
<path
|
|
||||||
d="m512 256c0 141.386719-114.613281 256-256 256s-256-114.613281-256-256 114.613281-256 256-256 256 114.613281 256 256zm0 0"
|
|
||||||
fill="#005ed3"
|
|
||||||
id="path4556"/>
|
|
||||||
<path
|
|
||||||
d="m 512,256 c 0,-11.71094 -0.80469,-23.23047 -2.32422,-34.52344 L 382.48047,94.28125 320.52344,121.85938 256,56.933594 212.69531,131.30469 129.51953,94.28125 141.86719,178.42187 49.949219,193.81641 114.32031,256 l -64.371091,62.18359 82.121091,82.16016 -2.55078,17.375 91.95703,91.95703 C 232.76953,511.19531 244.28906,512 256,512 397.38672,512 512,397.38672 512,256 Z"
|
|
||||||
id="path4558"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
style="fill:#00459f"
|
|
||||||
sodipodi:nodetypes="scccccccccccccss"/>
|
|
||||||
<path
|
|
||||||
d="m256 86.742188 37.109375 63.738281 70.574219-31.414063-10.527344 71.71875 77.078125 12.910156-54.144531 52.304688 54.144531 52.304688-77.078125 12.910156 10.527344 71.71875-70.574219-31.414063-37.109375 63.738281-37.109375-63.738281-70.574219 31.414063 10.527344-71.71875-77.078125-12.910156 54.144531-52.304688-54.144531-52.304688 77.078125-12.910156-10.527344-71.71875 70.574219 31.414063zm0 0"
|
|
||||||
fill="#ff0335"
|
|
||||||
id="path4560"/>
|
|
||||||
<path
|
|
||||||
d="m430.230469 308.300781-77.070313 12.910157 10.519532 71.71875-70.570313-31.410157-37.109375 63.742188v-338.523438l37.109375 63.742188 70.570313-31.410157-6.757813 46.101563-3.761719 25.617187 58.800782 9.851563 18.269531 3.058594-13.390625 12.929687-40.75 39.371094 11.378906 10.988281zm0 0"
|
|
||||||
fill="#c2001b"
|
|
||||||
id="path4562"/>
|
|
||||||
<path
|
|
||||||
d="m256 455.066406-43.304688-74.371094-83.175781 37.023438 12.347657-84.140625-91.917969-15.394531 64.371093-62.183594-64.371093-62.183594 91.917969-15.394531-12.347657-84.140625 83.179688 37.023438 43.300781-74.371094 43.304688 74.371094 83.175781-37.023438-12.347657 84.140625 91.917969 15.394531-64.371093 62.183594 64.371093 62.183594-91.917969 15.398437 12.347657 84.136719-83.175781-37.023438zm-30.917969-112.722656 30.917969 53.101562 30.917969-53.101562 57.964843 25.800781-8.703124-59.292969 62.238281-10.425781-43.917969-42.425781 43.917969-42.425781-62.238281-10.425781 8.703124-59.292969-57.964843 25.800781-30.917969-53.101562-30.917969 53.101562-57.964843-25.800781 8.703124 59.292969-62.238281 10.425781 43.917969 42.425781-43.917969 42.425781 62.238281 10.425781-8.703124 59.292969zm0 0"
|
|
||||||
fill="#ffdf47"
|
|
||||||
id="path4564"/>
|
|
||||||
<path
|
|
||||||
d="m403.308594 261.441406-5.628906-5.441406 25.160156-24.300781 39.210937-37.878907-55.75-9.339843-36.171875-6.058594 2.800782-19.09375 9.550781-65.046875-83.179688 37.019531-43.300781-74.371093v59.621093l30.921875 53.109375 57.957031-25.808594-3.910156 26.667969-2.546875 17.378907-2.242187 15.25 2.480468.421874 59.761719 10.007813-43.921875 42.421875 16.96875 16.390625 26.953125 26.03125-62.242187 10.429687 8.699218 59.296876-57.957031-25.808594-30.921875 53.109375v59.621093l43.300781-74.371093 83.179688 37.019531-12.351563-84.140625 91.921875-15.398437zm0 0"
|
|
||||||
fill="#fec000"
|
|
||||||
id="path4566"/>
|
|
||||||
<g
|
|
||||||
aria-label="K"
|
|
||||||
transform="matrix(1.1590846,-0.34467221,0.22789693,0.794981,0,0)"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:296.55969238px;line-height:125%;font-family:Impact;-inkscape-font-specification:Impact;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="text4596">
|
|
||||||
<path
|
|
||||||
d="m 220.91497,266.9035 -34.89789,105.85211 38.2284,128.58643 H 161.2555 L 136.63873,400.84769 V 501.34204 H 75.676021 V 266.9035 h 60.962709 v 91.08205 l 27.07845,-91.08205 z"
|
|
||||||
style="font-size:296.55969238px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="path824"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 477 B |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.1 KiB |
@ -1,40 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="50mm" height="50mm" viewBox="0 0 50 50">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="b">
|
|
||||||
<stop offset="0" style="stop-color:#348878;stop-opacity:1"/>
|
|
||||||
<stop offset="1" style="stop-color:#52bca6;stop-opacity:1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="a">
|
|
||||||
<stop offset="0" style="stop-color:#348878;stop-opacity:1"/>
|
|
||||||
<stop offset="1" style="stop-color:#56bda8;stop-opacity:1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient xlink:href="#a" id="e" x1="160.722" x2="168.412" y1="128.533" y2="134.326" gradientTransform="matrix(3.74959 0 0 3.74959 -541.79 -387.599)" gradientUnits="userSpaceOnUse"/>
|
|
||||||
<linearGradient xlink:href="#b" id="c" x1=".034" x2="50.319" y1="0" y2="50.285" gradientTransform="matrix(.99434 0 0 .99434 -.034 0)" gradientUnits="userSpaceOnUse"/>
|
|
||||||
<filter id="d" width="1.176" height="1.211" x="-.076" y="-.092" style="color-interpolation-filters:sRGB">
|
|
||||||
<feFlood flood-color="#fff" flood-opacity=".192" result="flood"/>
|
|
||||||
<feComposite in="flood" in2="SourceGraphic" operator="in" result="composite1"/>
|
|
||||||
<feGaussianBlur in="composite1" result="blur" stdDeviation="4"/>
|
|
||||||
<feOffset dx="3" dy="2.954" result="offset"/>
|
|
||||||
<feComposite in="SourceGraphic" in2="offset" result="composite2"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
<g style="display:inline">
|
|
||||||
<path d="M0 0h50v50H0z" style="fill:url(#c);fill-opacity:1;stroke:none;stroke-width:.286502;stroke-linejoin:bevel"/>
|
|
||||||
</g>
|
|
||||||
<g style="display:inline">
|
|
||||||
<path d="M50.4 46.883c-9.168 0-17.023 7.214-17.023 16.387v.007l.09 71.37-2.303 16.992 31.313-8.319h77.841c9.17 0 17.024-7.224 17.024-16.396V63.27c0-9.17-7.85-16.383-17.016-16.387h-.008zm0 11.566h89.926c3.222.004 5.45 2.347 5.45 4.82v63.655c0 2.475-2.232 4.82-5.457 4.82h-79.54l-15.908 4.807.162-.938-.088-72.343c0-2.476 2.23-4.82 5.455-4.82z" style="color:#fff;display:inline;fill:#fff;stroke:none;stroke-width:1.93113;-inkscape-stroke:none;filter:url(#d)" transform="scale(.26458)"/>
|
|
||||||
</g>
|
|
||||||
<g style="display:inline">
|
|
||||||
<path d="M88.2 95.309H64.92c-1.601 0-2.91 1.236-2.91 2.746l.022 18.602-.435 2.506 6.231-1.881H88.2c1.6 0 2.91-1.236 2.91-2.747v-16.48c0-1.51-1.31-2.746-2.91-2.746z" style="color:#fff;fill:url(#e);stroke:none;stroke-width:2.49558;-inkscape-stroke:none" transform="translate(-51.147 -81.516)"/>
|
|
||||||
<path d="M50.4 46.883c-9.168 0-17.023 7.214-17.023 16.387v.007l.09 71.37-2.303 16.992 31.313-8.319h77.841c9.17 0 17.024-7.224 17.024-16.396V63.27c0-9.17-7.85-16.383-17.016-16.387h-.008zm0 11.566h89.926c3.222.004 5.45 2.347 5.45 4.82v63.655c0 2.475-2.232 4.82-5.457 4.82h-79.54l-15.908 4.807.162-.938-.088-72.343c0-2.476 2.23-4.82 5.455-4.82z" style="color:#fff;fill:#fff;stroke:none;stroke-width:1.93113;-inkscape-stroke:none" transform="scale(.26458)"/>
|
|
||||||
<g style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0;word-spacing:0;fill:#fff;stroke:none;stroke-width:.525121">
|
|
||||||
<path d="M62.57 116.77v-1.312l3.28-1.459q.159-.068.306-.102.158-.045.283-.068l.271-.022v-.09q-.136-.012-.271-.046-.125-.023-.283-.057-.147-.045-.306-.113l-3.28-1.459v-1.323l5.068 2.319v1.413z" style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.45366 0 0 1.72815 -75.122 -171.953)"/>
|
|
||||||
<path d="M62.309 110.31v1.903l3.437 1.53.022.007-.022.008-3.437 1.53v1.892l.37-.17 5.221-2.39v-1.75zm.525.817 4.541 2.08v1.076l-4.541 2.078v-.732l3.12-1.389.003-.002a1.56 1.56 0 0 1 .258-.086h.006l.008-.002c.094-.027.176-.047.246-.06l.498-.041v-.574l-.24-.02a1.411 1.411 0 0 1-.231-.04l-.008-.001-.008-.002a9.077 9.077 0 0 1-.263-.053 2.781 2.781 0 0 1-.266-.097l-.004-.002-3.119-1.39z"
|
|
||||||
style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.45366 0 0 1.72815 -75.122 -171.953)"/>
|
|
||||||
</g>
|
|
||||||
<g style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0;word-spacing:0;fill:#fff;stroke:none;stroke-width:.525121">
|
|
||||||
<path d="M69.171 117.754h5.43v1.278h-5.43Z" style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.44935 0 0 1.66414 -74.104 -166.906)"/>
|
|
||||||
<path d="M68.908 117.492v1.802h5.955v-1.802zm.526.524h4.904v.754h-4.904z" style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.44935 0 0 1.66414 -74.104 -166.906)"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 66 KiB |
@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Tranga</title>
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
||||||
<link rel="stylesheet" href="styles/index.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="index.jsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,7 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { createRoot } from 'react-dom/client';
|
|
||||||
import App from './App.tsx';
|
|
||||||
|
|
||||||
const domNode = document.getElementById('app');
|
|
||||||
const root = createRoot(domNode);
|
|
||||||
root.render(<App />);
|
|
Before Width: | Height: | Size: 124 KiB |
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="#0F1729"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 804 B |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 440 B |
@ -1,43 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
height="512pt"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
width="512pt"
|
|
||||||
version="1.1"
|
|
||||||
id="svg4586">
|
|
||||||
<path
|
|
||||||
d="m512 256c0 141.386719-114.613281 256-256 256s-256-114.613281-256-256 114.613281-256 256-256 256 114.613281 256 256zm0 0"
|
|
||||||
fill="#005ed3"
|
|
||||||
id="path4556"/>
|
|
||||||
<path
|
|
||||||
d="m 512,256 c 0,-11.71094 -0.80469,-23.23047 -2.32422,-34.52344 L 382.48047,94.28125 320.52344,121.85938 256,56.933594 212.69531,131.30469 129.51953,94.28125 141.86719,178.42187 49.949219,193.81641 114.32031,256 l -64.371091,62.18359 82.121091,82.16016 -2.55078,17.375 91.95703,91.95703 C 232.76953,511.19531 244.28906,512 256,512 397.38672,512 512,397.38672 512,256 Z"
|
|
||||||
id="path4558"
|
|
||||||
style="fill:#00459f"/>
|
|
||||||
<path
|
|
||||||
d="m256 86.742188 37.109375 63.738281 70.574219-31.414063-10.527344 71.71875 77.078125 12.910156-54.144531 52.304688 54.144531 52.304688-77.078125 12.910156 10.527344 71.71875-70.574219-31.414063-37.109375 63.738281-37.109375-63.738281-70.574219 31.414063 10.527344-71.71875-77.078125-12.910156 54.144531-52.304688-54.144531-52.304688 77.078125-12.910156-10.527344-71.71875 70.574219 31.414063zm0 0"
|
|
||||||
fill="#ff0335"
|
|
||||||
id="path4560"/>
|
|
||||||
<path
|
|
||||||
d="m430.230469 308.300781-77.070313 12.910157 10.519532 71.71875-70.570313-31.410157-37.109375 63.742188v-338.523438l37.109375 63.742188 70.570313-31.410157-6.757813 46.101563-3.761719 25.617187 58.800782 9.851563 18.269531 3.058594-13.390625 12.929687-40.75 39.371094 11.378906 10.988281zm0 0"
|
|
||||||
fill="#c2001b"
|
|
||||||
id="path4562"/>
|
|
||||||
<path
|
|
||||||
d="m256 455.066406-43.304688-74.371094-83.175781 37.023438 12.347657-84.140625-91.917969-15.394531 64.371093-62.183594-64.371093-62.183594 91.917969-15.394531-12.347657-84.140625 83.179688 37.023438 43.300781-74.371094 43.304688 74.371094 83.175781-37.023438-12.347657 84.140625 91.917969 15.394531-64.371093 62.183594 64.371093 62.183594-91.917969 15.398437 12.347657 84.136719-83.175781-37.023438zm-30.917969-112.722656 30.917969 53.101562 30.917969-53.101562 57.964843 25.800781-8.703124-59.292969 62.238281-10.425781-43.917969-42.425781 43.917969-42.425781-62.238281-10.425781 8.703124-59.292969-57.964843 25.800781-30.917969-53.101562-30.917969 53.101562-57.964843-25.800781 8.703124 59.292969-62.238281 10.425781 43.917969 42.425781-43.917969 42.425781 62.238281 10.425781-8.703124 59.292969zm0 0"
|
|
||||||
fill="#ffdf47"
|
|
||||||
id="path4564"/>
|
|
||||||
<path
|
|
||||||
d="m403.308594 261.441406-5.628906-5.441406 25.160156-24.300781 39.210937-37.878907-55.75-9.339843-36.171875-6.058594 2.800782-19.09375 9.550781-65.046875-83.179688 37.019531-43.300781-74.371093v59.621093l30.921875 53.109375 57.957031-25.808594-3.910156 26.667969-2.546875 17.378907-2.242187 15.25 2.480468.421874 59.761719 10.007813-43.921875 42.421875 16.96875 16.390625 26.953125 26.03125-62.242187 10.429687 8.699218 59.296876-57.957031-25.808594-30.921875 53.109375v59.621093l43.300781-74.371093 83.179688 37.019531-12.351563-84.140625 91.921875-15.398437zm0 0"
|
|
||||||
fill="#fec000"
|
|
||||||
id="path4566"/>
|
|
||||||
<g
|
|
||||||
aria-label="K"
|
|
||||||
transform="matrix(1.1590846,-0.34467221,0.22789693,0.794981,0,0)"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:296.55969238px;line-height:125%;font-family:Impact;-inkscape-font-specification:Impact;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="text4596">
|
|
||||||
<path
|
|
||||||
d="m 220.91497,266.9035 -34.89789,105.85211 38.2284,128.58643 H 161.2555 L 136.63873,400.84769 V 501.34204 H 75.676021 V 266.9035 h 60.962709 v 91.08205 l 27.07845,-91.08205 z"
|
|
||||||
style="font-size:296.55969238px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="path824"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 477 B |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.1 KiB |
@ -1,40 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="50mm" height="50mm" viewBox="0 0 50 50">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="b">
|
|
||||||
<stop offset="0" style="stop-color:#348878;stop-opacity:1"/>
|
|
||||||
<stop offset="1" style="stop-color:#52bca6;stop-opacity:1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="a">
|
|
||||||
<stop offset="0" style="stop-color:#348878;stop-opacity:1"/>
|
|
||||||
<stop offset="1" style="stop-color:#56bda8;stop-opacity:1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient xlink:href="#a" id="e" x1="160.722" x2="168.412" y1="128.533" y2="134.326" gradientTransform="matrix(3.74959 0 0 3.74959 -541.79 -387.599)" gradientUnits="userSpaceOnUse"/>
|
|
||||||
<linearGradient xlink:href="#b" id="c" x1=".034" x2="50.319" y1="0" y2="50.285" gradientTransform="matrix(.99434 0 0 .99434 -.034 0)" gradientUnits="userSpaceOnUse"/>
|
|
||||||
<filter id="d" width="1.176" height="1.211" x="-.076" y="-.092" style="color-interpolation-filters:sRGB">
|
|
||||||
<feFlood flood-color="#fff" flood-opacity=".192" result="flood"/>
|
|
||||||
<feComposite in="flood" in2="SourceGraphic" operator="in" result="composite1"/>
|
|
||||||
<feGaussianBlur in="composite1" result="blur" stdDeviation="4"/>
|
|
||||||
<feOffset dx="3" dy="2.954" result="offset"/>
|
|
||||||
<feComposite in="SourceGraphic" in2="offset" result="composite2"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
<g style="display:inline">
|
|
||||||
<path d="M0 0h50v50H0z" style="fill:url(#c);fill-opacity:1;stroke:none;stroke-width:.286502;stroke-linejoin:bevel"/>
|
|
||||||
</g>
|
|
||||||
<g style="display:inline">
|
|
||||||
<path d="M50.4 46.883c-9.168 0-17.023 7.214-17.023 16.387v.007l.09 71.37-2.303 16.992 31.313-8.319h77.841c9.17 0 17.024-7.224 17.024-16.396V63.27c0-9.17-7.85-16.383-17.016-16.387h-.008zm0 11.566h89.926c3.222.004 5.45 2.347 5.45 4.82v63.655c0 2.475-2.232 4.82-5.457 4.82h-79.54l-15.908 4.807.162-.938-.088-72.343c0-2.476 2.23-4.82 5.455-4.82z" style="color:#fff;display:inline;fill:#fff;stroke:none;stroke-width:1.93113;-inkscape-stroke:none;filter:url(#d)" transform="scale(.26458)"/>
|
|
||||||
</g>
|
|
||||||
<g style="display:inline">
|
|
||||||
<path d="M88.2 95.309H64.92c-1.601 0-2.91 1.236-2.91 2.746l.022 18.602-.435 2.506 6.231-1.881H88.2c1.6 0 2.91-1.236 2.91-2.747v-16.48c0-1.51-1.31-2.746-2.91-2.746z" style="color:#fff;fill:url(#e);stroke:none;stroke-width:2.49558;-inkscape-stroke:none" transform="translate(-51.147 -81.516)"/>
|
|
||||||
<path d="M50.4 46.883c-9.168 0-17.023 7.214-17.023 16.387v.007l.09 71.37-2.303 16.992 31.313-8.319h77.841c9.17 0 17.024-7.224 17.024-16.396V63.27c0-9.17-7.85-16.383-17.016-16.387h-.008zm0 11.566h89.926c3.222.004 5.45 2.347 5.45 4.82v63.655c0 2.475-2.232 4.82-5.457 4.82h-79.54l-15.908 4.807.162-.938-.088-72.343c0-2.476 2.23-4.82 5.455-4.82z" style="color:#fff;fill:#fff;stroke:none;stroke-width:1.93113;-inkscape-stroke:none" transform="scale(.26458)"/>
|
|
||||||
<g style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0;word-spacing:0;fill:#fff;stroke:none;stroke-width:.525121">
|
|
||||||
<path d="M62.57 116.77v-1.312l3.28-1.459q.159-.068.306-.102.158-.045.283-.068l.271-.022v-.09q-.136-.012-.271-.046-.125-.023-.283-.057-.147-.045-.306-.113l-3.28-1.459v-1.323l5.068 2.319v1.413z" style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.45366 0 0 1.72815 -75.122 -171.953)"/>
|
|
||||||
<path d="M62.309 110.31v1.903l3.437 1.53.022.007-.022.008-3.437 1.53v1.892l.37-.17 5.221-2.39v-1.75zm.525.817 4.541 2.08v1.076l-4.541 2.078v-.732l3.12-1.389.003-.002a1.56 1.56 0 0 1 .258-.086h.006l.008-.002c.094-.027.176-.047.246-.06l.498-.041v-.574l-.24-.02a1.411 1.411 0 0 1-.231-.04l-.008-.001-.008-.002a9.077 9.077 0 0 1-.263-.053 2.781 2.781 0 0 1-.266-.097l-.004-.002-3.119-1.39z"
|
|
||||||
style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.45366 0 0 1.72815 -75.122 -171.953)"/>
|
|
||||||
</g>
|
|
||||||
<g style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0;word-spacing:0;fill:#fff;stroke:none;stroke-width:.525121">
|
|
||||||
<path d="M69.171 117.754h5.43v1.278h-5.43Z" style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.44935 0 0 1.66414 -74.104 -166.906)"/>
|
|
||||||
<path d="M68.908 117.492v1.802h5.955v-1.802zm.526.524h4.904v.754h-4.904z" style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.44935 0 0 1.66414 -74.104 -166.906)"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 4.6 KiB |
@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
width="800px" height="800px" viewBox="0 0 971.986 971.986"
|
|
||||||
xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<path d="M370.216,459.3c10.2,11.1,15.8,25.6,15.8,40.6v442c0,26.601,32.1,40.101,51.1,21.4l123.3-141.3
|
|
||||||
c16.5-19.8,25.6-29.601,25.6-49.2V500c0-15,5.7-29.5,15.8-40.601L955.615,75.5c26.5-28.8,6.101-75.5-33.1-75.5h-873
|
|
||||||
c-39.2,0-59.7,46.6-33.1,75.5L370.216,459.3z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 714 B |
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
|
|
||||||
<title>
|
|
||||||
external link
|
|
||||||
</title>
|
|
||||||
<path fill="#fff" d="M6 1h5v5L8.86 3.85 4.7 8 4 7.3l4.15-4.16L6 1Z M2 3h2v1H2v6h6V8h1v2a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 304 B |
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
width="800px" height="800px" viewBox="0 0 93.5 93.5" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path d="M93.5,40.899c0-2.453-1.995-4.447-4.448-4.447H81.98c-0.74-2.545-1.756-5.001-3.035-7.331l4.998-5
|
|
||||||
c0.826-0.827,1.303-1.973,1.303-3.146c0-1.19-0.462-2.306-1.303-3.146L75.67,9.555c-1.613-1.615-4.673-1.618-6.29,0l-5,5
|
|
||||||
c-2.327-1.28-4.786-2.296-7.332-3.037v-7.07C57.048,1.995,55.053,0,52.602,0H40.899c-2.453,0-4.447,1.995-4.447,4.448v7.071
|
|
||||||
c-2.546,0.741-5.005,1.757-7.333,3.037l-5-5c-1.68-1.679-4.609-1.679-6.288,0L9.555,17.83c-1.734,1.734-1.734,4.555,0,6.289
|
|
||||||
l4.999,5c-1.279,2.33-2.295,4.788-3.036,7.333h-7.07C1.995,36.452,0,38.447,0,40.899V52.6c0,2.453,1.995,4.447,4.448,4.447h7.071
|
|
||||||
c0.74,2.545,1.757,5.003,3.036,7.332l-4.998,4.999c-0.827,0.827-1.303,1.974-1.303,3.146c0,1.189,0.462,2.307,1.302,3.146
|
|
||||||
l8.274,8.273c1.614,1.615,4.674,1.619,6.29,0l5-5c2.328,1.279,4.786,2.297,7.333,3.037v7.071c0,2.453,1.995,4.448,4.447,4.448
|
|
||||||
h11.702c2.453,0,4.446-1.995,4.446-4.448V81.98c2.546-0.74,5.005-1.756,7.332-3.037l5,5c1.681,1.68,4.608,1.68,6.288,0
|
|
||||||
l8.275-8.273c1.734-1.734,1.734-4.555,0-6.289l-4.998-5.001c1.279-2.329,2.295-4.787,3.035-7.332h7.071
|
|
||||||
c2.453,0,4.448-1.995,4.448-4.446V40.899z M62.947,46.75c0,8.932-7.266,16.197-16.197,16.197c-8.931,0-16.197-7.266-16.197-16.197
|
|
||||||
c0-8.931,7.266-16.197,16.197-16.197C55.682,30.553,62.947,37.819,62.947,46.75z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.7 KiB |
@ -1,19 +0,0 @@
|
|||||||
import React, {ReactElement, useEffect} from "react";
|
|
||||||
import {getData} from "../../App";
|
|
||||||
import IAuthor from "../types/IAuthor";
|
|
||||||
|
|
||||||
export default function AuthorElement({apiUri, authorId} : {apiUri: string, authorId: string | null}) : ReactElement{
|
|
||||||
let [author, setAuthor] = React.useState<IAuthor | null>(null);
|
|
||||||
|
|
||||||
useEffect(()=> {
|
|
||||||
if(authorId === null)
|
|
||||||
return;
|
|
||||||
getData(`${apiUri}/v2/Query/AuthorTag/${authorId}`)
|
|
||||||
.then((json) => {
|
|
||||||
let ret = json as IAuthor;
|
|
||||||
setAuthor(ret);
|
|
||||||
});
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (<span className="Manga-Author-Name">{author ? author.authorName : authorId}</span>);
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
import React, {ReactElement, ReactEventHandler, useEffect, useState} from "react";
|
|
||||||
import Manga from "../api/Manga";
|
|
||||||
import Chapter from "../api/Chapter";
|
|
||||||
import IChapter from "../types/IChapter";
|
|
||||||
import IManga from "../types/IManga";
|
|
||||||
|
|
||||||
export default function ChapterItem({apiUri, chapterId} : {apiUri: string, chapterId: string}) : ReactElement {
|
|
||||||
const setCoverItem : ReactEventHandler<HTMLImageElement> = (e) => {
|
|
||||||
setMangaCoverHtmlImageItem(e.currentTarget);
|
|
||||||
}
|
|
||||||
|
|
||||||
let [chapter, setChapter] = useState<IChapter | null>(null);
|
|
||||||
let [manga, setManga] = useState<IManga | null>(null);
|
|
||||||
let [mangaCoverUrl, setMangaCoverUrl] = useState<string>("../../media/blahaj.png");
|
|
||||||
let [mangaCoverHtmlImageItem, setMangaCoverHtmlImageItem] = useState<HTMLImageElement | null>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
Chapter.GetChapterFromId(apiUri, chapterId).then(setChapter);
|
|
||||||
}, []);
|
|
||||||
useEffect(() => {
|
|
||||||
if(chapter === null)
|
|
||||||
manga = null;
|
|
||||||
else
|
|
||||||
Manga.GetMangaById(apiUri, chapter.parentMangaId).then(setManga);
|
|
||||||
}, [chapter]);
|
|
||||||
useEffect(() => {
|
|
||||||
if(chapter != null && mangaCoverHtmlImageItem != null)
|
|
||||||
setMangaCoverUrl(Manga.GetMangaCoverImageUrl(apiUri, chapter.parentMangaId, mangaCoverHtmlImageItem));
|
|
||||||
}, [chapter, mangaCoverHtmlImageItem]);
|
|
||||||
|
|
||||||
let [clicked, setClicked] = useState<boolean>(false);
|
|
||||||
|
|
||||||
return (<div className="ChapterItem" key={chapterId} is-clicked={clicked ? "clicked" : "not-clicked"} onClick={() => setClicked(!clicked)}>
|
|
||||||
<img className="ChapterItem-Cover" src={mangaCoverUrl} alt="Manga Cover" onLoad={setCoverItem} onResize={setCoverItem}></img>
|
|
||||||
<p className="ChapterItem-MangaName">{manga ? manga.name : "Manga-Name"}</p>
|
|
||||||
<p className="ChapterItem-ChapterName">{chapter ? chapter.title : "Chapter-Title"}</p>
|
|
||||||
<p className="ChapterItem-Volume">Vol.{chapter ? chapter.volumeNumber : "VolNum"}</p>
|
|
||||||
<p className="ChapterItem-Chapter">Ch.{chapter ? chapter.chapterNumber : "ChNum"}</p>
|
|
||||||
<p className="ChapterItem-VolumeChapter">Vol.{chapter ? chapter.volumeNumber : "VolNum"} Ch.{chapter ? chapter.chapterNumber : "ChNum"}</p>
|
|
||||||
<a className="ChapterItem-Website" href={chapter ? chapter.url : "#"}><img src="../../media/link.svg" alt="Link"/></a>
|
|
||||||
</div>)
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import {ReactElement, useState} from "react";
|
|
||||||
import NotificationConnector from "../api/NotificationConnector";
|
|
||||||
import Loader from "../Loader";
|
|
||||||
import IGotifyRecord from "../types/records/IGotifyRecord";
|
|
||||||
import {isValidUri} from "../../App";
|
|
||||||
|
|
||||||
export function GotifyItem ({apiUri} : {apiUri: string}) : ReactElement{
|
|
||||||
const [record, setRecord] = useState<IGotifyRecord>({
|
|
||||||
endpoint: "",
|
|
||||||
appToken: "",
|
|
||||||
priority: 3
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
return <div className="NotificationConnectorItem">
|
|
||||||
<input className="NotificationConnectorItem-Name" value="Gotify" disabled={true} />
|
|
||||||
<div className="NotificationConnectorItem-Url">
|
|
||||||
<input type="text" className="NotificationConnectorItem-RequestUrl" placeholder="URL" onChange={(e) => setRecord({...record, endpoint: e.currentTarget.value})} />
|
|
||||||
<input type="text" className="NotificationConnectorItem-AppToken" placeholder="Apptoken" onChange={(e) => setRecord({...record, appToken: e.currentTarget.value})} />
|
|
||||||
</div>
|
|
||||||
<div className="NotificationConnectorItem-Priority">
|
|
||||||
<label htmlFor="NotificationConnectorItem-Priority">Priority</label>
|
|
||||||
<input id="NotificationConnectorItem-Priority-Value" type="number" className="NotificationConnectorItem-Priority-Value" min={1} max={5} defaultValue={3} onChange={(e) => setRecord({...record, priority: e.currentTarget.valueAsNumber})} />
|
|
||||||
</div>
|
|
||||||
<>
|
|
||||||
<button className="NotificationConnectorItem-Save" onClick={(e) => {
|
|
||||||
if(record === null || Validate(record) === false)
|
|
||||||
return;
|
|
||||||
setLoading(true);
|
|
||||||
NotificationConnector.CreateGotify(apiUri, record)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}}>Add</button>
|
|
||||||
<Loader loading={loading} style={{width:"40px",height:"40px"}}/>
|
|
||||||
</>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Validate(record: IGotifyRecord) : boolean {
|
|
||||||
if(!isValidUri(record.endpoint))
|
|
||||||
return false;
|
|
||||||
if(record.appToken.length < 1)
|
|
||||||
return false;
|
|
||||||
if(record.priority < 1 || record.priority > 5)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import React, {ReactElement, useEffect} from "react";
|
|
||||||
import {getData} from "../../App";
|
|
||||||
import ILink from "../types/ILink";
|
|
||||||
|
|
||||||
export default function LinkElement({apiUri, linkId} : {apiUri: string, linkId: string | null}) : ReactElement{
|
|
||||||
let [link, setLink] = React.useState<ILink | null>(null);
|
|
||||||
|
|
||||||
useEffect(()=> {
|
|
||||||
if(linkId === null)
|
|
||||||
return;
|
|
||||||
getData(`${apiUri}/v2/Query/Link/${linkId}`)
|
|
||||||
.then((json) => {
|
|
||||||
let ret = json as ILink;
|
|
||||||
setLink(ret);
|
|
||||||
});
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (<a className="Manga-Link-Value" href={link ? link.linkUrl : "#"}>{link ? link.linkProvider : linkId}</a>);
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
import {ReactElement, useState} from "react";
|
|
||||||
import INewLibraryRecord, {Validate} from "../types/records/INewLibraryRecord";
|
|
||||||
import Loader from "../Loader";
|
|
||||||
import LocalLibrary from "../api/LocalLibrary";
|
|
||||||
import "../../styles/localLibrary.css";
|
|
||||||
import ILocalLibrary from "../types/ILocalLibrary";
|
|
||||||
|
|
||||||
export default function LocalLibraryItem({apiUri, library} : {apiUri: string, library: ILocalLibrary | null}) : ReactElement {
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [record, setRecord] = useState<INewLibraryRecord>({
|
|
||||||
path: library?.basePath ?? "",
|
|
||||||
name: library?.libraryName ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return (<div className="LocalLibraryFunctions">
|
|
||||||
<label htmlFor="LocalLibraryFunctions-Name">Library Name</label>
|
|
||||||
<input id="LocalLibraryFunctions-Name" className="LocalLibraryFunctions-Name" placeholder="Library Name" defaultValue={library ? library.libraryName : "New Library"}
|
|
||||||
onChange={(e) => setRecord({...record, name: e.currentTarget.value})}/>
|
|
||||||
<label htmlFor="LocalLibraryFunctions-Path">Library Path</label>
|
|
||||||
<input id="LocalLibraryFunctions-Path" className="LocalLibraryFunctions-Path" placeholder="Library Path" defaultValue={library ? library.basePath : ""}
|
|
||||||
onChange={(e) => setRecord({...record, path: e.currentTarget.value})}/>
|
|
||||||
{library
|
|
||||||
? <button className="LocalLibraryFunctions-Action" onClick={() => {
|
|
||||||
if(record === null || Validate(record) === false)
|
|
||||||
return;
|
|
||||||
setLoading(true);
|
|
||||||
LocalLibrary.UpdateLibrary(apiUri, library.localLibraryId, record)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}}>Edit</button>
|
|
||||||
: <button className="LocalLibraryFunctions-Action" onClick={() => {
|
|
||||||
if(record === null || Validate(record) === false)
|
|
||||||
return;
|
|
||||||
setLoading(true);
|
|
||||||
LocalLibrary.CreateLibrary(apiUri, record)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}}>Add</button>
|
|
||||||
}
|
|
||||||
<Loader loading={loading} style={{width:"40px",height:"40px"}}/>
|
|
||||||
</div>);
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
import Manga from "../api/Manga";
|
|
||||||
import React, {Children, ReactElement, ReactEventHandler, useEffect, useState} from "react";
|
|
||||||
import Icon from '@mdi/react';
|
|
||||||
import { mdiTagTextOutline, mdiAccountEdit, mdiLinkVariant } from '@mdi/js';
|
|
||||||
import MarkdownPreview from '@uiw/react-markdown-preview';
|
|
||||||
import Loader from "../Loader";
|
|
||||||
import IManga from "../types/IManga";
|
|
||||||
import IChapter from "../types/IChapter";
|
|
||||||
import AuthorElement from "./Author";
|
|
||||||
import LinkElement from "./Link";
|
|
||||||
|
|
||||||
export default function MangaItem({apiUri, mangaId, children} : {apiUri: string, mangaId: string, children?: (string | ReactElement)[]}) : ReactElement {
|
|
||||||
const LoadMangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
|
|
||||||
if(e.currentTarget.src != Manga.GetMangaCoverImageUrl(apiUri, mangaId, e.currentTarget))
|
|
||||||
e.currentTarget.src = Manga.GetMangaCoverImageUrl(apiUri, mangaId, e.currentTarget);
|
|
||||||
}
|
|
||||||
|
|
||||||
let [manga, setManga] = useState<IManga | null>(null);
|
|
||||||
let [clicked, setClicked] = useState<boolean>(false);
|
|
||||||
let [latestChapterDownloaded, setLatestChapterDownloaded] = useState<IChapter | null>(null);
|
|
||||||
let [latestChapterAvailable, setLatestChapterAvailable] = useState<IChapter | null>(null);
|
|
||||||
let [loadingChapterStats, setLoadingChapterStats] = useState<boolean>(true);
|
|
||||||
let [settingThreshold, setSettingThreshold] = useState<boolean>(false);
|
|
||||||
const invalidTargets = ["input", "textarea", "button", "select", "a"];
|
|
||||||
useEffect(() => {
|
|
||||||
Manga.GetMangaById(apiUri, mangaId).then(setManga);
|
|
||||||
Manga.GetLatestChapterDownloaded(apiUri, mangaId)
|
|
||||||
.then(setLatestChapterDownloaded)
|
|
||||||
.finally(() => {
|
|
||||||
if(latestChapterDownloaded && latestChapterAvailable)
|
|
||||||
setLoadingChapterStats(false);
|
|
||||||
});
|
|
||||||
Manga.GetLatestChapterAvailable(apiUri, mangaId)
|
|
||||||
.then(setLatestChapterAvailable)
|
|
||||||
.finally(() => {
|
|
||||||
if(latestChapterDownloaded && latestChapterAvailable)
|
|
||||||
setLoadingChapterStats(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (<div className="MangaItem" key={mangaId} is-clicked={clicked ? "clicked" : "not-clicked"} onClick={(e)=> {
|
|
||||||
e.preventDefault();
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if(invalidTargets.find(x => x == target.localName) === undefined )
|
|
||||||
setClicked(!clicked)
|
|
||||||
}}>
|
|
||||||
<img className="MangaItem-Cover" src="../../media/blahaj.png" alt="Manga Cover" onLoad={LoadMangaCover} onResize={LoadMangaCover}></img>
|
|
||||||
<div className="MangaItem-Connector">{manga ? manga.mangaConnectorId : "Connector"}</div>
|
|
||||||
<div className="MangaItem-Status" release-status={manga?.releaseStatus}></div>
|
|
||||||
<div className="MangaItem-Name">{manga ? manga.name : "Name"}</div>
|
|
||||||
<a className="MangaItem-Website" href={manga ? manga.websiteUrl : "#"}><img src="../../media/link.svg" alt="Link"/></a>
|
|
||||||
<div className="MangaItem-Tags">
|
|
||||||
{manga ? manga.authorIds.map(authorId =>
|
|
||||||
<div className="MangaItem-Author" key={authorId} >
|
|
||||||
<Icon path={mdiAccountEdit} size={0.5} />
|
|
||||||
<AuthorElement apiUri={apiUri} authorId={authorId}></AuthorElement>
|
|
||||||
</div>)
|
|
||||||
:
|
|
||||||
<div className="MangaItem-Author" key="null-AuthorTag">
|
|
||||||
<Icon path={mdiAccountEdit} size={0.5} />
|
|
||||||
<AuthorElement apiUri={apiUri} authorId={null}></AuthorElement>
|
|
||||||
</div>}
|
|
||||||
{manga ? manga.tags.map(tag =>
|
|
||||||
<div className="MangaItem-Tag" key={tag}>
|
|
||||||
<Icon path={mdiTagTextOutline} size={0.5}/>
|
|
||||||
<span className="MangaItem-Tag-Value">{tag}</span>
|
|
||||||
</div>)
|
|
||||||
:
|
|
||||||
<div className="MangaItem-Tag" key="null-Tag">
|
|
||||||
<Icon path={mdiTagTextOutline} size={0.5}/>
|
|
||||||
<span className="MangaItem-Tag-Value">Tag</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{manga ? manga.linkIds.map(linkId =>
|
|
||||||
<div className="MangaItem-Link" key={linkId}>
|
|
||||||
<Icon path={mdiLinkVariant} size={0.5}/>
|
|
||||||
<LinkElement apiUri={apiUri} linkId={linkId} />
|
|
||||||
</div>)
|
|
||||||
:
|
|
||||||
<div className="MangaItem-Link" key="null-Link">
|
|
||||||
<Icon path={mdiLinkVariant} size={0.5}/>
|
|
||||||
<LinkElement apiUri={apiUri} linkId={null} />
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
<MarkdownPreview className="MangaItem-Description" source={manga ? manga.description : "# Description"} />
|
|
||||||
<div className="MangaItem-Props">
|
|
||||||
<div className="MangaItem-Props-Threshold">
|
|
||||||
Start at Chapter
|
|
||||||
<input type="text" defaultValue={latestChapterDownloaded ? latestChapterDownloaded.chapterNumber : ""} disabled={settingThreshold} onChange={(e) => {
|
|
||||||
setSettingThreshold(true);
|
|
||||||
Manga.SetIgnoreThreshold(apiUri, mangaId, e.currentTarget.valueAsNumber).finally(()=>setSettingThreshold(false));
|
|
||||||
}} />
|
|
||||||
<Loader loading={settingThreshold} style={{margin: "-10px -45px"}}/>
|
|
||||||
out of <span className="MangaItem-Props-Threshold-Available">{latestChapterAvailable ? latestChapterAvailable.chapterNumber : <Loader loading={loadingChapterStats} style={{margin: "-10px -35px"}} />}</span>
|
|
||||||
</div>
|
|
||||||
{children ? children.map(c => {
|
|
||||||
if(c instanceof Element)
|
|
||||||
return c as ReactElement;
|
|
||||||
else
|
|
||||||
return c
|
|
||||||
}) : null}
|
|
||||||
</div>
|
|
||||||
</div>)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import React, {ReactElement, useEffect} from "react";
|
|
||||||
import {getData} from "../../App";
|
|
||||||
import IMangaAltTitle from "../types/IMangaAltTitle";
|
|
||||||
|
|
||||||
export default function AltTitleElement({apiUri, altTitleId} : {apiUri: string, altTitleId: string | null}) : ReactElement{
|
|
||||||
let [altTitle, setAltTitle] = React.useState<IMangaAltTitle | null>(null);
|
|
||||||
|
|
||||||
useEffect(()=> {
|
|
||||||
if(altTitleId === null)
|
|
||||||
return;
|
|
||||||
getData(`${apiUri}/v2/Query/AltTitle/${altTitleId}`)
|
|
||||||
.then((json) => {
|
|
||||||
let ret = json as IMangaAltTitle;
|
|
||||||
setAltTitle(ret);
|
|
||||||
});
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (<span className="Manga-AltTitle">{altTitle ? altTitle.title : altTitleId}</span>);
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
import {ReactElement, ReactEventHandler, useState} from "react";
|
|
||||||
import "../../styles/notificationConnector.css";
|
|
||||||
import Loader from "../Loader";
|
|
||||||
import NotificationConnector from "../api/NotificationConnector";
|
|
||||||
import INotificationConnector from "../types/INotificationConnector";
|
|
||||||
import {GotifyItem} from "./Gotify";
|
|
||||||
import {NtfyItem} from "./Ntfy";
|
|
||||||
import {PushoverItem} from "./Pushover";
|
|
||||||
|
|
||||||
export default function NotificationConnectorItem({apiUri, notificationConnector} : {apiUri: string, notificationConnector: INotificationConnector | null}) : ReactElement {
|
|
||||||
if(notificationConnector != null)
|
|
||||||
return <DefaultItem apiUri={apiUri} notificationConnector={notificationConnector} />
|
|
||||||
|
|
||||||
const [selectedConnectorElement, setSelectedConnectorElement] = useState<ReactElement>(<DefaultItem apiUri={apiUri} notificationConnector={null} />);
|
|
||||||
|
|
||||||
return <div>
|
|
||||||
<div>New Notification Connector</div>
|
|
||||||
<label>Type</label>
|
|
||||||
<select defaultValue="default" onChange={(e) => {
|
|
||||||
switch (e.currentTarget.value){
|
|
||||||
case "default": setSelectedConnectorElement(<DefaultItem apiUri={apiUri} notificationConnector={null} />); break;
|
|
||||||
case "gotify": setSelectedConnectorElement(<GotifyItem apiUri={apiUri} />); break;
|
|
||||||
case "ntfy": setSelectedConnectorElement(<NtfyItem apiUri={apiUri} />); break;
|
|
||||||
case "pushover": setSelectedConnectorElement(<PushoverItem apiUri={apiUri} />); break;
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<option value="default">Generic REST</option>
|
|
||||||
<option value="gotify">Gotify</option>
|
|
||||||
<option value="ntfy">Ntfy</option>
|
|
||||||
<option value="pushover">Pushover</option>
|
|
||||||
</select>
|
|
||||||
{selectedConnectorElement}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DefaultItem({apiUri, notificationConnector}:{apiUri: string, notificationConnector: INotificationConnector | null}) : ReactElement {
|
|
||||||
const AddHeader : ReactEventHandler<HTMLButtonElement> = () => {
|
|
||||||
let header : Record<string, string> = {};
|
|
||||||
let x = info;
|
|
||||||
x.headers = [header, ...x.headers];
|
|
||||||
setInfo(x);
|
|
||||||
setHeaderElements([...headerElements, <HeaderElement record={header} />])
|
|
||||||
}
|
|
||||||
const [headerElements, setHeaderElements] = useState<ReactElement[]>([]);
|
|
||||||
const [info, setInfo] = useState<INotificationConnector>({
|
|
||||||
name: "",
|
|
||||||
url: "",
|
|
||||||
headers: [],
|
|
||||||
httpMethod: "",
|
|
||||||
body: ""
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
return <div className="NotificationConnectorItem">
|
|
||||||
<input className="NotificationConnectorItem-Name" placeholder="Name" defaultValue={notificationConnector ? notificationConnector.name : ""}
|
|
||||||
disabled={notificationConnector != null} onChange={(e) => setInfo({...info, name: e.currentTarget.value})} />
|
|
||||||
<div className="NotificationConnectorItem-Url">
|
|
||||||
<select className="NotificationConnectorItem-RequestMethod" defaultValue={notificationConnector ? notificationConnector.httpMethod : ""}
|
|
||||||
disabled={notificationConnector != null} onChange={(e)=> setInfo({...info, httpMethod: e.currentTarget.value})} >
|
|
||||||
<option value="" disabled hidden>Request Method</option>
|
|
||||||
<option value="GET">GET</option>
|
|
||||||
<option value="POST">POST</option>
|
|
||||||
</select>
|
|
||||||
<input type="url" className="NotificationConnectorItem-RequestUrl" placeholder="URL" defaultValue={notificationConnector ? notificationConnector.url : ""}
|
|
||||||
disabled={notificationConnector != null} onChange={(e) => setInfo({...info, url: e.currentTarget.value})} />
|
|
||||||
</div>
|
|
||||||
<textarea className="NotificationConnectorItem-Body" placeholder="Request-Body" defaultValue={notificationConnector ? notificationConnector.body : ""}
|
|
||||||
disabled={notificationConnector != null} onChange={(e)=> setInfo({...info, body: e.currentTarget.value})} />
|
|
||||||
{notificationConnector != null ? null :
|
|
||||||
(
|
|
||||||
<p className="NotificationConnectorItem-Explanation">Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</p>
|
|
||||||
)}
|
|
||||||
<div className="NotificationConnectorItem-Headers">
|
|
||||||
{headerElements}
|
|
||||||
{notificationConnector
|
|
||||||
? notificationConnector.headers.map((h: Record<string, string>) =>
|
|
||||||
(<HeaderElement record={h} disabled={notificationConnector != null}/>)
|
|
||||||
) :
|
|
||||||
(
|
|
||||||
<button className="NotificationConnectorItem-AddHeader" onClick={AddHeader}>Add Header</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<>
|
|
||||||
<button className="NotificationConnectorItem-Save" onClick={(e) => {
|
|
||||||
setLoading(true);
|
|
||||||
NotificationConnector.CreateNotificationConnector(apiUri, info)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}}>Add</button>
|
|
||||||
<Loader loading={loading} style={{width:"40px",height:"40px",margin:"25vh calc(sin(70)*(50% - 40px))"}}/>
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function HeaderElement({record, disabled} : {record: Record<string, string>, disabled?: boolean | null}) : ReactElement {
|
|
||||||
return <div className="NotificationConnectorItem-Header" key={record.name}>
|
|
||||||
<input type="text" className="NotificationConnectorItem-Header-Key" placeholder="Header-Key" defaultValue={record.name} disabled={disabled?disabled:false} onChange={(e) => record.name = e.currentTarget.value}/>
|
|
||||||
<input type="text" className="NotificationConnectorItem-Header-Value" placeholder="Header-Value" defaultValue={record.value} disabled={disabled?disabled:false} onChange={(e) => record.value = e.currentTarget.value} />
|
|
||||||
</div>;
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
import {isValidUri} from "../../App";
|
|
||||||
import {ReactElement, useState} from "react";
|
|
||||||
import NotificationConnector from "../api/NotificationConnector";
|
|
||||||
import Loader from "../Loader";
|
|
||||||
import INtfyRecord from "../types/records/INtfyRecord";
|
|
||||||
|
|
||||||
export function NtfyItem ({apiUri} : {apiUri: string}) : ReactElement{
|
|
||||||
const [info, setInfo] = useState<INtfyRecord>({
|
|
||||||
endpoint: "",
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
topic: "",
|
|
||||||
priority: 0
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
return <div className="NotificationConnectorItem">
|
|
||||||
<input className="NotificationConnectorItem-Name" value="Ntfy" disabled={true} />
|
|
||||||
<div className="NotificationConnectorItem-Url">
|
|
||||||
<input type="text" className="NotificationConnectorItem-RequestUrl" placeholder="URL" onChange={(e) => setInfo({...info, endpoint: e.currentTarget.value})} />
|
|
||||||
<input type="text" className="NotificationConnectorItem-Topic" placeholder="Topic" onChange={(e) => setInfo({...info, topic: e.currentTarget.value})} />
|
|
||||||
</div>
|
|
||||||
<div className="NotificationConnectorItem-Ident">
|
|
||||||
<input type="text" className="NotificationConnectorItem-Username" placeholder="Username" onChange={(e) => setInfo({...info, username: e.currentTarget.value})} />
|
|
||||||
<input type="password" className="NotificationConnectorItem-Password" placeholder="***" onChange={(e) => setInfo({...info, password: e.currentTarget.value})} />
|
|
||||||
</div>
|
|
||||||
<div className="NotificationConnectorItem-Priority">
|
|
||||||
<label htmlFor="NotificationConnectorItem-Priority">Priority</label>
|
|
||||||
<input id="NotificationConnectorItem-Priority-Value" type="number" className="NotificationConnectorItem-Priority-Value" min={1} max={5} defaultValue={3} onChange={(e) => setInfo({...info, priority: e.currentTarget.valueAsNumber})} />
|
|
||||||
</div><>
|
|
||||||
<button className="NotificationConnectorItem-Save" onClick={(e) => {
|
|
||||||
if(info === null || Validate(info) === false)
|
|
||||||
return;
|
|
||||||
setLoading(true);
|
|
||||||
NotificationConnector.CreateNtfy(apiUri, info)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}}>Add</button>
|
|
||||||
<Loader loading={loading} style={{width:"40px",height:"40px",margin:"25vh calc(sin(70)*(50% - 40px))"}}/>
|
|
||||||
</>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Validate(record: INtfyRecord) : boolean {
|
|
||||||
if(!isValidUri(record.endpoint))
|
|
||||||
return false;
|
|
||||||
if(record.username.length < 1)
|
|
||||||
return false;
|
|
||||||
if(record.password.length < 1)
|
|
||||||
return false;
|
|
||||||
if(record.topic.length < 1)
|
|
||||||
return false;
|
|
||||||
if(record.priority < 1 || record.priority > 5)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
import {ReactElement, useState} from "react";
|
|
||||||
import NotificationConnector from "../api/NotificationConnector";
|
|
||||||
import Loader from "../Loader";
|
|
||||||
import IPushoverRecord from "../types/records/IPushoverRecord";
|
|
||||||
|
|
||||||
export function PushoverItem ({apiUri} : {apiUri: string}) : ReactElement{
|
|
||||||
const [info, setInfo] = useState<IPushoverRecord>({
|
|
||||||
apptoken: "",
|
|
||||||
user: ""
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
return <div className="NotificationConnectorItem">
|
|
||||||
<input className="NotificationConnectorItem-Name" value="Pushover" disabled={true} />
|
|
||||||
<div className="NotificationConnectorItem-Ident">
|
|
||||||
<input type="text" className="NotificationConnectorItem-Apptoken" placeholder="Apptoken" onChange={(e) => setInfo({...info, apptoken: e.currentTarget.value})} />
|
|
||||||
<input type="text" className="NotificationConnectorItem-User" placeholder="User" onChange={(e) => setInfo({...info, user: e.currentTarget.value})} />
|
|
||||||
</div>
|
|
||||||
<>
|
|
||||||
<button className="NotificationConnectorItem-Save" onClick={(e) => {
|
|
||||||
if(info === null || Validate(info) === false)
|
|
||||||
return;
|
|
||||||
setLoading(true);
|
|
||||||
NotificationConnector.CreatePushover(apiUri, info)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}}>Add</button>
|
|
||||||
<Loader loading={loading} style={{width:"40px",height:"40px",margin:"25vh calc(sin(70)*(50% - 40px))"}}/>
|
|
||||||
</>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Validate(record: IPushoverRecord) : boolean {
|
|
||||||
if(record.apptoken.length < 1)
|
|
||||||
return false;
|
|
||||||
if(record.user.length < 1)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
import React, {useEffect} from 'react';
|
|
||||||
import '../styles/footer.css';
|
|
||||||
import Job from './api/Job';
|
|
||||||
import Icon from '@mdi/react';
|
|
||||||
import {mdiCounter, mdiEyeCheck, mdiRun, mdiTrayFull} from '@mdi/js';
|
|
||||||
import QueuePopUp from "./QueuePopUp";
|
|
||||||
import {JobState, JobType} from "./types/Jobs/IJob";
|
|
||||||
|
|
||||||
export default function Footer({connectedToBackend, apiUri, checkConnectedInterval} : {connectedToBackend: boolean, apiUri: string, checkConnectedInterval: number}) {
|
|
||||||
const [MonitoringJobsCount, setMonitoringJobsCount] = React.useState(0);
|
|
||||||
const [AllJobsCount, setAllJobsCount] = React.useState(0);
|
|
||||||
const [RunningJobsCount, setRunningJobsCount] = React.useState(0);
|
|
||||||
const [WaitingJobsCount, setWaitingJobs] = React.useState(0);
|
|
||||||
const [countUpdateInterval, setCountUpdateInterval] = React.useState<number | undefined>(undefined);
|
|
||||||
|
|
||||||
function UpdateBackendState(){
|
|
||||||
Job.GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob).then((jobs) => setMonitoringJobsCount(jobs.length));
|
|
||||||
Job.GetAllJobs(apiUri).then((jobs) => setAllJobsCount(jobs.length));
|
|
||||||
Job.GetJobsInState(apiUri, JobState.Running).then((jobs) => setRunningJobsCount(jobs.length));
|
|
||||||
Job.GetJobsInState(apiUri, JobState.Waiting).then((jobs) => setWaitingJobs(jobs.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(connectedToBackend){
|
|
||||||
UpdateBackendState();
|
|
||||||
if(countUpdateInterval === undefined){
|
|
||||||
setCountUpdateInterval(setInterval(() => {
|
|
||||||
UpdateBackendState();
|
|
||||||
}, checkConnectedInterval * 5));
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
clearInterval(countUpdateInterval);
|
|
||||||
setCountUpdateInterval(undefined);
|
|
||||||
}
|
|
||||||
}, [connectedToBackend]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<footer>
|
|
||||||
<div className="statusBadge" ><Icon path={mdiEyeCheck} size={1}/> <span>{MonitoringJobsCount}</span></div>
|
|
||||||
<span>+</span>
|
|
||||||
<QueuePopUp connectedToBackend={connectedToBackend} apiUri={apiUri} checkConnectedInterval={checkConnectedInterval}>
|
|
||||||
<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>{WaitingJobsCount}</span></div>
|
|
||||||
</QueuePopUp>
|
|
||||||
<span>=</span>
|
|
||||||
<div className="statusBadge"><Icon path={mdiCounter} size={1}/> <span>{AllJobsCount}</span></div>
|
|
||||||
<p id="madeWith">Made with Blåhaj 🦈</p>
|
|
||||||
</footer>)
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import '../styles/header.css'
|
|
||||||
import Settings from "./Settings";
|
|
||||||
import IFrontendSettings from "./types/IFrontendSettings";
|
|
||||||
|
|
||||||
export default function Header({backendConnected, apiUri, settings, setFrontendSettings} : {backendConnected: boolean, apiUri: string, settings: IFrontendSettings, setFrontendSettings: (settings: IFrontendSettings) => void}){
|
|
||||||
return (
|
|
||||||
<header>
|
|
||||||
<div id="titlebox">
|
|
||||||
<img alt="website image is Blahaj" src="../media/blahaj.png"/>
|
|
||||||
<span>Tranga</span>
|
|
||||||
</div>
|
|
||||||
<Settings backendConnected={backendConnected} apiUri={apiUri} frontendSettings={settings} setFrontendSettings={setFrontendSettings} />
|
|
||||||
</header>)
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import "../styles/loader.css";
|
|
||||||
import {CSSProperties} from "react";
|
|
||||||
|
|
||||||
export default function Loader({loading, style} : {loading: boolean, style?:CSSProperties|null}) {
|
|
||||||
return <span is-loading={loading ? "loading" : "done"} style={style ? style : undefined}></span>
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
import React, {ReactElement, useEffect, useState} from 'react';
|
|
||||||
import Job from './api/Job';
|
|
||||||
import '../styles/monitorMangaList.css';
|
|
||||||
import {JobType} from "./types/Jobs/IJob";
|
|
||||||
import '../styles/mangaCover.css'
|
|
||||||
import IDownloadAvailableChaptersJob from "./types/Jobs/IDownloadAvailableChaptersJob";
|
|
||||||
import Manga from "./api/Manga";
|
|
||||||
import MangaItem from "./Elements/Manga";
|
|
||||||
|
|
||||||
export default function MonitorJobsList({onStartSearch, connectedToBackend, apiUri, checkConnectedInterval} : {onStartSearch() : void, connectedToBackend: boolean, apiUri: string, checkConnectedInterval: number}) {
|
|
||||||
const [MonitoringJobs, setMonitoringJobs] = useState<IDownloadAvailableChaptersJob[]>([]);
|
|
||||||
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number | undefined>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(connectedToBackend) {
|
|
||||||
UpdateMonitoringJobsList();
|
|
||||||
if(joblistUpdateInterval === undefined){
|
|
||||||
setJoblistUpdateInterval(setInterval(() => {
|
|
||||||
UpdateMonitoringJobsList();
|
|
||||||
}, checkConnectedInterval));
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
clearInterval(joblistUpdateInterval);
|
|
||||||
setJoblistUpdateInterval(undefined);
|
|
||||||
}
|
|
||||||
}, [connectedToBackend]);
|
|
||||||
|
|
||||||
function UpdateMonitoringJobsList(){
|
|
||||||
if(!connectedToBackend)
|
|
||||||
return;
|
|
||||||
//console.debug("Updating MonitoringJobsList");
|
|
||||||
Job.GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob)
|
|
||||||
.then((jobs) => jobs as IDownloadAvailableChaptersJob[])
|
|
||||||
.then((jobs) => {
|
|
||||||
if(jobs.length != MonitoringJobs.length ||
|
|
||||||
MonitoringJobs.filter(j => jobs.find(nj => nj.jobId == j.jobId)).length > 1 ||
|
|
||||||
jobs.filter(nj => MonitoringJobs.find(j => nj.jobId == j.jobId)).length > 1){
|
|
||||||
setMonitoringJobs(jobs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function StartSearchMangaEntry() : ReactElement {
|
|
||||||
return (<div key="monitorMangaEntry.StartSearch" className="startSearchEntry MangaItem" onClick={onStartSearch}>
|
|
||||||
<img className="MangaItem-Cover" src="../media/blahaj.png" alt="Blahaj"></img>
|
|
||||||
<div>
|
|
||||||
<div style={{margin: "30px auto", color: "black", textShadow: "1px 2px #f5a9b8"}} className="MangaItem-Name">Add new Manga</div>
|
|
||||||
<div style={{fontSize: "42pt", textAlign: "center", textShadow: "1px 2px #5bcefa"}}>+</div>
|
|
||||||
</div>
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="MonitorMangaList">
|
|
||||||
<StartSearchMangaEntry />
|
|
||||||
{MonitoringJobs.map((MonitoringJob) =>
|
|
||||||
<MangaItem apiUri={apiUri} mangaId={MonitoringJob.mangaId} key={MonitoringJob.mangaId}>
|
|
||||||
<></>
|
|
||||||
<button className="Manga-DeleteButton" onClick={() => {
|
|
||||||
Manga.DeleteManga(apiUri, MonitoringJob.mangaId);
|
|
||||||
}}>Delete</button>
|
|
||||||
</MangaItem>
|
|
||||||
)}
|
|
||||||
</div>);
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
import React, {ReactElement, useEffect, useState} from 'react';
|
|
||||||
import IJob, {JobState, JobType} from "./types/Jobs/IJob";
|
|
||||||
import '../styles/queuePopUp.css';
|
|
||||||
import '../styles/popup.css';
|
|
||||||
import Job from "./api/Job";
|
|
||||||
import IDownloadSingleChapterJob from "./types/Jobs/IDownloadSingleChapterJob";
|
|
||||||
import ChapterItem from "./Elements/Chapter";
|
|
||||||
|
|
||||||
export default function QueuePopUp({connectedToBackend, children, apiUri, checkConnectedInterval} : {connectedToBackend: boolean, children: ReactElement[], apiUri: string, checkConnectedInterval: number}) {
|
|
||||||
|
|
||||||
const [WaitingJobs, setWaitingJobs] = React.useState<IJob[]>([]);
|
|
||||||
const [RunningJobs, setRunningJobs] = React.useState<IJob[]>([]);
|
|
||||||
const [showQueuePopup, setShowQueuePopup] = useState<boolean>(false);
|
|
||||||
const [queueListInterval, setQueueListInterval] = React.useState<number | undefined>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(connectedToBackend && showQueuePopup) {
|
|
||||||
UpdateMonitoringJobsList();
|
|
||||||
if(queueListInterval === undefined){
|
|
||||||
setQueueListInterval(setInterval(() => {
|
|
||||||
UpdateMonitoringJobsList();
|
|
||||||
}, checkConnectedInterval));
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
clearInterval(queueListInterval);
|
|
||||||
setQueueListInterval(undefined);
|
|
||||||
}
|
|
||||||
}, [connectedToBackend, showQueuePopup]);
|
|
||||||
|
|
||||||
function UpdateMonitoringJobsList(){
|
|
||||||
Job.GetJobsInState(apiUri, JobState.Waiting)
|
|
||||||
.then((jobs: IJob[]) => {
|
|
||||||
//console.log(jobs);
|
|
||||||
return jobs;
|
|
||||||
})
|
|
||||||
.then(setWaitingJobs);
|
|
||||||
Job.GetJobsInState(apiUri, JobState.Running)
|
|
||||||
.then((jobs: IJob[]) => {
|
|
||||||
//console.log(jobs);
|
|
||||||
return jobs;
|
|
||||||
})
|
|
||||||
.then(setRunningJobs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
<div onClick={() => setShowQueuePopup(true)}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
{showQueuePopup
|
|
||||||
? <div className="popup" id="QueuePopUp">
|
|
||||||
<div className="popupHeader">
|
|
||||||
<h1>Queue Status</h1>
|
|
||||||
<img alt="Close Queue" className="close" src="../media/close-x.svg"
|
|
||||||
onClick={() => setShowQueuePopup(false)}/>
|
|
||||||
</div>
|
|
||||||
<div id="QueuePopUpBody" className="popupBody">
|
|
||||||
<div>
|
|
||||||
{RunningJobs.filter(j => j.jobType == JobType.DownloadSingleChapterJob).map(j => {
|
|
||||||
let job = j as IDownloadSingleChapterJob;
|
|
||||||
return <ChapterItem apiUri={apiUri} chapterId={job.chapterId} />
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{WaitingJobs.filter(j => j.jobType == JobType.DownloadSingleChapterJob).map(j =>{
|
|
||||||
let job = j as IDownloadSingleChapterJob;
|
|
||||||
return <ChapterItem apiUri={apiUri} chapterId={job.chapterId} />
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,158 +0,0 @@
|
|||||||
import React, {ChangeEventHandler, EventHandler, useEffect, useState} from 'react';
|
|
||||||
import {MangaConnector} from "./api/MangaConnector";
|
|
||||||
import IMangaConnector from "./types/IMangaConnector";
|
|
||||||
import {isValidUri} from "../App";
|
|
||||||
import '../styles/search.css';
|
|
||||||
import Job from "./api/Job";
|
|
||||||
import LocalLibrary from "./api/LocalLibrary";
|
|
||||||
import Loader from "./Loader";
|
|
||||||
import IManga from "./types/IManga";
|
|
||||||
import SearchFunctions from "./api/Search";
|
|
||||||
import ILocalLibrary from "./types/ILocalLibrary";
|
|
||||||
import MangaItem from "./Elements/Manga";
|
|
||||||
|
|
||||||
export default function Search({apiUri, jobInterval, closeSearch} : {apiUri: string, jobInterval: Date, closeSearch(): void}) {
|
|
||||||
let [loading, setLoading] = useState<boolean>(true);
|
|
||||||
const [mangaConnectors, setConnectors] = useState<IMangaConnector[]>();
|
|
||||||
const [selectedConnector, setSelectedConnector] = useState<IMangaConnector>();
|
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState<string>();
|
|
||||||
const [searchBoxValue, setSearchBoxValue] = useState("");
|
|
||||||
const [searchResults, setSearchResults] = useState<IManga[]>();
|
|
||||||
|
|
||||||
const pattern = /https:\/\/([a-z0-9.]+\.[a-z0-9]{2,})(?:\/.*)?/i
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
MangaConnector.GetAllConnectors(apiUri).then((connectors)=> {
|
|
||||||
return connectors.filter(c => c.enabled);
|
|
||||||
}).then(setConnectors).then(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedConnector(mangaConnectors?.find(c => c.name == "Global"));
|
|
||||||
setSelectedLanguage(mangaConnectors?.find(c => c.name == "Global")?.supportedLanguages[0])
|
|
||||||
}, [mangaConnectors]);
|
|
||||||
|
|
||||||
const selectedConnectorChanged : ChangeEventHandler<HTMLSelectElement> = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if(mangaConnectors === undefined)
|
|
||||||
return;
|
|
||||||
const selectedConnector = mangaConnectors.find((con: IMangaConnector) => con.name == event.target.value);
|
|
||||||
if(selectedConnector === undefined)
|
|
||||||
return;
|
|
||||||
setSelectedConnector(selectedConnector);
|
|
||||||
setSelectedLanguage(selectedConnector.supportedLanguages[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchBoxValueChanged : ChangeEventHandler<HTMLInputElement> = (event) => {
|
|
||||||
event.currentTarget.style.width = event.target.value.length + "ch";
|
|
||||||
if(mangaConnectors === undefined)
|
|
||||||
return;
|
|
||||||
var str : string = event.target.value;
|
|
||||||
setSearchBoxValue(str);
|
|
||||||
if(isValidUri(str))
|
|
||||||
setSelectedConnector(undefined);
|
|
||||||
const match = str.match(pattern);
|
|
||||||
if(match === null)
|
|
||||||
return;
|
|
||||||
let baseUri = match[1];
|
|
||||||
const selectCon = mangaConnectors.find((con: IMangaConnector) => {
|
|
||||||
return con.baseUris.find(uri => uri == baseUri);
|
|
||||||
});
|
|
||||||
if(selectCon != undefined){
|
|
||||||
setSelectedConnector(selectCon);
|
|
||||||
setSelectedLanguage(selectCon.supportedLanguages[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExecuteSearch : EventHandler<any> = () => {
|
|
||||||
if(searchBoxValue.length < 1 || selectedConnector === undefined || selectedLanguage === ""){
|
|
||||||
console.error("Tried initiating search while not all fields where submitted.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//console.info(`Searching Name: ${searchBoxValue} Connector: ${selectedConnector.name} Language: ${selectedLanguage}`);
|
|
||||||
if(isValidUri(searchBoxValue) && !selectedConnector.baseUris.find((uri: string) => {
|
|
||||||
const match = searchBoxValue.match(pattern);
|
|
||||||
if(match === null)
|
|
||||||
return false;
|
|
||||||
return match[1] == uri
|
|
||||||
}))
|
|
||||||
{
|
|
||||||
console.error("URL in Searchbox detected, but does not match selected connector.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(true);
|
|
||||||
if(!isValidUri(searchBoxValue)){
|
|
||||||
SearchFunctions.SearchNameOnConnector(apiUri, selectedConnector.name, searchBoxValue)
|
|
||||||
.then((mangas: IManga[]) => {
|
|
||||||
setSearchResults(mangas);
|
|
||||||
})
|
|
||||||
.finally(()=>setLoading(false));
|
|
||||||
}else{
|
|
||||||
SearchFunctions.SearchUrl(apiUri, searchBoxValue)
|
|
||||||
.then((manga: IManga) => {
|
|
||||||
setSearchResults([manga]);
|
|
||||||
})
|
|
||||||
.finally(()=>setLoading(false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeSelectedLanguage : ChangeEventHandler<HTMLSelectElement> = (event) => setSelectedLanguage(event.target.value);
|
|
||||||
let [selectedLibrary, setSelectedLibrary] = useState<ILocalLibrary | null>(null);
|
|
||||||
let [libraries, setLibraries] = useState<ILocalLibrary[] | null>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
LocalLibrary.GetLibraries(apiUri).then(setLibraries);
|
|
||||||
}, []);
|
|
||||||
useEffect(() => {
|
|
||||||
if(libraries === null || libraries.length < 1)
|
|
||||||
setSelectedLibrary(null);
|
|
||||||
else
|
|
||||||
setSelectedLibrary(libraries[0]);
|
|
||||||
}, [libraries]);
|
|
||||||
|
|
||||||
const selectedLibraryChanged : ChangeEventHandler<HTMLSelectElement> = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if(libraries === null)
|
|
||||||
return;
|
|
||||||
const selectedLibrary = libraries.find((lib:ILocalLibrary) => lib.localLibraryId == event.target.value);
|
|
||||||
if(selectedLibrary === undefined)
|
|
||||||
return;
|
|
||||||
setSelectedLibrary(selectedLibrary);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<div id="Search">
|
|
||||||
<div id="SearchBox">
|
|
||||||
<input type="text" placeholder="Manganame" id="Searchbox-Manganame" onKeyDown={(e) => {if(e.key == "Enter") ExecuteSearch(null);}} onChange={searchBoxValueChanged} disabled={loading} />
|
|
||||||
<select id="Searchbox-Connector" value={selectedConnector === undefined ? "" : selectedConnector.name} onChange={selectedConnectorChanged} disabled={loading}>
|
|
||||||
{mangaConnectors === undefined ? <option value="Loading">Loading</option> : <option value="" disabled hidden>Select</option>}
|
|
||||||
{mangaConnectors === undefined
|
|
||||||
? null
|
|
||||||
: mangaConnectors.map(con => <option value={con.name} key={con.name}>{con.name}</option>)}
|
|
||||||
</select>
|
|
||||||
<select id="Searchbox-language" onChange={changeSelectedLanguage} value={selectedLanguage === null ? "" : selectedLanguage} disabled={loading}>
|
|
||||||
{mangaConnectors === undefined ? <option value="Loading">Loading</option> : <option value="" disabled hidden>Select Connector</option>}
|
|
||||||
{selectedConnector === undefined
|
|
||||||
? null
|
|
||||||
: selectedConnector.supportedLanguages.map(language => <option value={language} key={language}>{language}</option>)}
|
|
||||||
</select>
|
|
||||||
<button id="Searchbox-button" type="submit" onClick={ExecuteSearch} disabled={loading}>Search</button>
|
|
||||||
<Loader loading={loading} style={{width:"40px", height:"40px", zIndex: 50}}/>
|
|
||||||
</div>
|
|
||||||
<img alt="Close Search" id="closeSearch" src="../media/close-x.svg" onClick={closeSearch} />
|
|
||||||
<div id="SearchResults">
|
|
||||||
{searchResults === undefined
|
|
||||||
? null
|
|
||||||
: searchResults.map(result => {
|
|
||||||
return <MangaItem apiUri={apiUri} mangaId={result.mangaId} >
|
|
||||||
<select defaultValue={selectedLibrary === null ? "" : selectedLibrary.localLibraryId} onChange={selectedLibraryChanged}>
|
|
||||||
{selectedLibrary === null || libraries === null ? <option value="">Loading</option>
|
|
||||||
: libraries.map(library => <option key={library.localLibraryId} value={library.localLibraryId}>{library.libraryName} ({library.basePath})</option>)}
|
|
||||||
</select>
|
|
||||||
<button className="Manga-AddButton" onClick={() => {
|
|
||||||
Job.CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {recurrenceTimeMs: new Date(jobInterval).getTime(), localLibraryId: selectedLibrary!.localLibraryId});
|
|
||||||
}}>Monitor</button>
|
|
||||||
</MangaItem>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>);
|
|
||||||
}
|
|
@ -1,199 +0,0 @@
|
|||||||
import '../styles/settings.css';
|
|
||||||
import '../styles/react-toggle.css';
|
|
||||||
import React, {useEffect, useRef, useState} from "react";
|
|
||||||
import NotificationConnector from "./api/NotificationConnector";
|
|
||||||
import IBackendSettings from "./types/IBackendSettings";
|
|
||||||
import BackendSettings from "./api/BackendSettings";
|
|
||||||
import Toggle from "react-toggle";
|
|
||||||
import Loader from "./Loader";
|
|
||||||
import IMangaConnector from "./types/IMangaConnector";
|
|
||||||
import {MangaConnector} from "./api/MangaConnector";
|
|
||||||
import IFrontendSettings from "./types/IFrontendSettings";
|
|
||||||
import INotificationConnector from "./types/INotificationConnector";
|
|
||||||
import ILocalLibrary from "./types/ILocalLibrary";
|
|
||||||
import LocalLibrary from "./api/LocalLibrary";
|
|
||||||
import {RequestLimitType} from "./types/EnumRequestLimitType";
|
|
||||||
import NotificationConnectorItem from "./Elements/NotificationConnector";
|
|
||||||
import LocalLibraryItem from "./Elements/LocalLibrary";
|
|
||||||
|
|
||||||
export default function Settings({ backendConnected, apiUri, frontendSettings, setFrontendSettings } : {
|
|
||||||
backendConnected: boolean,
|
|
||||||
apiUri: string,
|
|
||||||
frontendSettings: IFrontendSettings,
|
|
||||||
setFrontendSettings: (settings: IFrontendSettings) => void
|
|
||||||
}) {
|
|
||||||
const [showSettings, setShowSettings] = useState<boolean>(false);
|
|
||||||
const [loadingBackend, setLoadingBackend] = useState(false);
|
|
||||||
const [backendSettings, setBackendSettings] = useState<IBackendSettings|null>(null);
|
|
||||||
const [notificationConnectors, setNotificationConnectors] = useState<INotificationConnector[]>([]);
|
|
||||||
const [mangaConnectors,setMangaConnectors] = useState<IMangaConnector[]>([]);
|
|
||||||
const [localLibraries, setLocalLibraries] = useState<ILocalLibrary[]>([]);
|
|
||||||
const [chapterNamingScheme, setChapterNamingScheme] = useState<string>("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(!backendConnected)
|
|
||||||
return;
|
|
||||||
NotificationConnector.GetNotificationConnectors(apiUri).then(setNotificationConnectors);
|
|
||||||
LocalLibrary.GetLibraries(apiUri).then(setLocalLibraries);
|
|
||||||
BackendSettings.GetSettings(apiUri).then(setBackendSettings);
|
|
||||||
MangaConnector.GetAllConnectors(apiUri).then(setMangaConnectors);
|
|
||||||
}, [backendConnected, showSettings]);
|
|
||||||
|
|
||||||
const dateToStr = (x: Date) => {
|
|
||||||
const ret = (x.getHours() < 10 ? "0" + x.getHours() : x.getHours())
|
|
||||||
+ ":" +
|
|
||||||
(x.getMinutes() < 10 ? "0" + x.getMinutes() : x.getMinutes());
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChangeRequestLimit = (requestType: RequestLimitType, limit: number) => {
|
|
||||||
if(backendSettings === null)
|
|
||||||
return;
|
|
||||||
setLoadingBackend(true);
|
|
||||||
BackendSettings.UpdateRequestLimit(apiUri, requestType, limit)
|
|
||||||
.then(() => setBackendSettings({...backendSettings, [requestType]: requestType}))
|
|
||||||
.finally(() => setLoadingBackend(false));
|
|
||||||
}
|
|
||||||
const ref : React.LegacyRef<HTMLInputElement> | undefined = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="Settings">
|
|
||||||
<div onClick={() => setShowSettings(true)}>
|
|
||||||
<img id="Settings-Cogwheel" src="../media/settings-cogwheel.svg" alt="settings-cogwheel" />
|
|
||||||
</div>
|
|
||||||
{showSettings
|
|
||||||
? <div className="popup" id="SettingsPopUp">
|
|
||||||
<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">
|
|
||||||
<Loader loading={loadingBackend} style={{width: "64px", height: "64px", margin: "25vh calc(sin(70)*(50% - 40px))", zIndex: 100, padding: 0, borderRadius: "50%", border: 0, minWidth: "initial", maxWidth: "initial"}}/>
|
|
||||||
<div className="settings-apiuri">
|
|
||||||
<h3>ApiUri</h3>
|
|
||||||
<input type="url" defaultValue={frontendSettings.apiUri} onChange={(e) => setFrontendSettings({...frontendSettings, apiUri:e.currentTarget.value})} id="ApiUri" />
|
|
||||||
</div>
|
|
||||||
<div className="settings-jobinterval">
|
|
||||||
<h3>Default Job-Interval</h3>
|
|
||||||
<input type="time" min="00:30" max="23:59" defaultValue={dateToStr(new Date(frontendSettings.jobInterval))} onChange={(e) => setFrontendSettings({...frontendSettings, jobInterval: new Date(e.currentTarget.valueAsNumber-60*60*1000) ?? frontendSettings.jobInterval})}/>
|
|
||||||
</div>
|
|
||||||
<div className={"settings-chapterNamingScheme"}>
|
|
||||||
<h3>Chapter Naming-Scheme</h3>
|
|
||||||
<input type={"text"} placeholder={backendSettings?.chapterNamingScheme} onChange={(e) => setChapterNamingScheme(e.target.value)} />
|
|
||||||
<button type={"button"} onClick={() => {
|
|
||||||
setLoadingBackend(true);
|
|
||||||
BackendSettings.UpdateChapterNamingScheme(apiUri, chapterNamingScheme).finally(() => setLoadingBackend(false));
|
|
||||||
}}>Submit</button>
|
|
||||||
</div>
|
|
||||||
<div className="settings-bwimages">
|
|
||||||
<h3>B/W Images</h3>
|
|
||||||
<Toggle defaultChecked={backendSettings ? backendSettings.bwImages : false} disabled={backendSettings ? false : !loadingBackend}
|
|
||||||
onChange={(e) => {
|
|
||||||
if(backendSettings === null)
|
|
||||||
return;
|
|
||||||
setLoadingBackend(true);
|
|
||||||
BackendSettings.UpdateBWImageToggle(apiUri, e.target.checked)
|
|
||||||
.then(() => setBackendSettings({...backendSettings, bwImages: e.target.checked}))
|
|
||||||
.finally(() => setLoadingBackend(false));
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
<div className="settings-aprilfools">
|
|
||||||
<h3>April Fools Mode</h3>
|
|
||||||
<Toggle defaultChecked={backendSettings ? backendSettings.aprilFoolsMode : false} disabled={backendSettings ? false : !loadingBackend}
|
|
||||||
onChange={(e) => {
|
|
||||||
if(backendSettings === null)
|
|
||||||
return;
|
|
||||||
setLoadingBackend(true);
|
|
||||||
BackendSettings.UpdateAprilFoolsToggle(apiUri, e.target.checked)
|
|
||||||
.then(() => setBackendSettings({...backendSettings, aprilFoolsMode: e.target.checked}))
|
|
||||||
.finally(() => setLoadingBackend(false));
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
<div className="settings-imagecompression">
|
|
||||||
<h3>Image Compression</h3>
|
|
||||||
<Toggle defaultChecked={backendSettings ? backendSettings.compression < 100 : false} disabled={backendSettings ? false : !loadingBackend}
|
|
||||||
onChange={(e) => {
|
|
||||||
if(backendSettings === null)
|
|
||||||
return;
|
|
||||||
setLoadingBackend(true);
|
|
||||||
BackendSettings.UpdateImageCompressionValue(apiUri, e.target.checked ? 40 : 100)
|
|
||||||
.then(() => setBackendSettings({...backendSettings, compression: e.target.checked ? 40 : 100}))
|
|
||||||
.then(() => {
|
|
||||||
if(ref.current != null){
|
|
||||||
ref.current.value = e.target.checked ? "40" : "100";
|
|
||||||
ref.current.disabled = !e.target.checked;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => setLoadingBackend(false));
|
|
||||||
}} />
|
|
||||||
<input ref={ref} type="number" min={0} max={100} defaultValue={backendSettings ? backendSettings.compression : 0} disabled={backendSettings ? false : !loadingBackend}
|
|
||||||
onChange={(e) => {
|
|
||||||
if(backendSettings === null)
|
|
||||||
return;
|
|
||||||
setLoadingBackend(true);
|
|
||||||
BackendSettings.UpdateImageCompressionValue(apiUri, e.currentTarget.valueAsNumber)
|
|
||||||
.then(() => setBackendSettings({...backendSettings, compression: e.currentTarget.valueAsNumber}))
|
|
||||||
.finally(() => setLoadingBackend(false));
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
<div className="settings-useragent">
|
|
||||||
<h3>User Agent</h3>
|
|
||||||
<input type="text" defaultValue={backendSettings ? backendSettings.userAgent : ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
if(backendSettings === null)
|
|
||||||
return;
|
|
||||||
setLoadingBackend(true);
|
|
||||||
BackendSettings.UpdateUserAgent(apiUri, e.currentTarget.value)
|
|
||||||
.then(() => setBackendSettings({...backendSettings, userAgent: e.currentTarget.value}))
|
|
||||||
.finally(() => setLoadingBackend(false));
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
<div className="settings-requestLimits">
|
|
||||||
<h3>Request Limits:</h3>
|
|
||||||
<label htmlFor="Default">Default</label>
|
|
||||||
<input id="Default" type="number" defaultValue={backendSettings ? backendSettings.requestLimits.Default : 0} disabled={backendSettings ? false : !loadingBackend}
|
|
||||||
onChange={(e) => ChangeRequestLimit(RequestLimitType.Default, e.currentTarget.valueAsNumber)} />
|
|
||||||
<label htmlFor="MangaInfo">MangaInfo</label>
|
|
||||||
<input id="MangaInfo" type="number" defaultValue={backendSettings ? backendSettings.requestLimits.MangaInfo : 0} disabled={backendSettings ? false : !loadingBackend}
|
|
||||||
onChange={(e) => ChangeRequestLimit(RequestLimitType.MangaInfo, e.currentTarget.valueAsNumber)} />
|
|
||||||
<label htmlFor="MangaDexFeed">MangaDexFeed</label>
|
|
||||||
<input id="MangaDexFeed" type="number" defaultValue={backendSettings ? backendSettings.requestLimits.MangaDexFeed : 0} disabled={backendSettings ? false : !loadingBackend}
|
|
||||||
onChange={(e) => ChangeRequestLimit(RequestLimitType.MangaDexFeed, e.currentTarget.valueAsNumber)} />
|
|
||||||
<label htmlFor="MangaDexImage">MangaDexImage</label>
|
|
||||||
<input id="MangaDexImage" type="number" defaultValue={backendSettings ? backendSettings.requestLimits.MangaDexImage : 0} disabled={backendSettings ? false : !loadingBackend}
|
|
||||||
onChange={(e) => ChangeRequestLimit(RequestLimitType.MangaDexImage, e.currentTarget.valueAsNumber)} />
|
|
||||||
<label htmlFor="MangaImage">MangaImage</label>
|
|
||||||
<input id="MangaImage" type="number" defaultValue={backendSettings ? backendSettings.requestLimits.MangaImage : 0} disabled={backendSettings ? false : !loadingBackend}
|
|
||||||
onChange={(e) => ChangeRequestLimit(RequestLimitType.MangaImage, e.currentTarget.valueAsNumber)} />
|
|
||||||
<label htmlFor="MangaCover">MangaCover</label>
|
|
||||||
<input id="MangaCover" type="number" defaultValue={backendSettings ? backendSettings.requestLimits.MangaCover : 0} disabled={backendSettings ? false : !loadingBackend}
|
|
||||||
onChange={(e) => ChangeRequestLimit(RequestLimitType.MangaCover, e.currentTarget.valueAsNumber)} />
|
|
||||||
</div>
|
|
||||||
<div className={"settings-mangaConnectors"}>
|
|
||||||
{mangaConnectors.map(mc => {
|
|
||||||
return (
|
|
||||||
<div key={mc.name}>
|
|
||||||
<span>{mc.name}</span>
|
|
||||||
<Toggle defaultChecked={mc.enabled} onChange={(e) => {
|
|
||||||
MangaConnector.SetConnectorEnabled(apiUri, mc.name, e.currentTarget.checked);
|
|
||||||
}} />
|
|
||||||
</div>);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>Notification Connectors:</h3>
|
|
||||||
{notificationConnectors.map(c => <NotificationConnectorItem apiUri={apiUri} notificationConnector={c} key={c.name} />)}
|
|
||||||
<NotificationConnectorItem apiUri={apiUri} notificationConnector={null} key="New Notification Connector" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>Local Libraries:</h3>
|
|
||||||
{localLibraries.map(l => <LocalLibraryItem apiUri={apiUri} library={l} key={l.localLibraryId} />)}
|
|
||||||
<LocalLibraryItem apiUri={apiUri} library={null} key="New Local Library" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
import {deleteData, getData, patchData} from "../../App";
|
|
||||||
import IRequestLimits from "../types/IRequestLimits";
|
|
||||||
import IBackendSettings from "../types/IBackendSettings";
|
|
||||||
import {RequestLimitType} from "../types/EnumRequestLimitType";
|
|
||||||
|
|
||||||
export default class BackendSettings {
|
|
||||||
static async GetSettings(apiUri: string) : Promise<IBackendSettings> {
|
|
||||||
return getData(`${apiUri}/v2/Settings`).then((s) => s as IBackendSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetUserAgent(apiUri: string) : Promise<string> {
|
|
||||||
return getData(`${apiUri}/v2/Settings/UserAgent`).then((text) => text as unknown as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async UpdateUserAgent(apiUri: string, userAgent: string) {
|
|
||||||
return patchData(`${apiUri}/v2/Settings/UserAgent`, userAgent);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async ResetUserAgent(apiUri: string) {
|
|
||||||
return deleteData(`${apiUri}/v2/Settings/UserAgent`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetRequestLimits(apiUri: string) : Promise<IRequestLimits> {
|
|
||||||
return getData(`${apiUri}/v2/Settings/RequestLimits`).then((limits) => limits as IRequestLimits);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async ResetRequestLimits(apiUri: string) {
|
|
||||||
return deleteData(`${apiUri}/v2/Settings/RequestLimits`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async UpdateRequestLimit(apiUri: string, requestType: RequestLimitType, value: number) {
|
|
||||||
return patchData(`${apiUri}/v2/Settings/RequestLimits/${requestType}`, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async ResetRequestLimit(apiUri: string, requestType: RequestLimitType) {
|
|
||||||
return deleteData(`${apiUri}/v2/Settings/RequestLimits/${requestType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetImageCompressionValue(apiUri: string) : Promise<number> {
|
|
||||||
return getData(`${apiUri}/v2/Settings/ImageCompression`).then((n) => n as unknown as number);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async UpdateImageCompressionValue(apiUri: string, value: number) {
|
|
||||||
return patchData(`${apiUri}/v2/Settings/ImageCompression`, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetBWImageToggle(apiUri: string) : Promise<boolean> {
|
|
||||||
return getData(`${apiUri}/v2/Settings/BWImages`).then((state) => state as unknown as boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async UpdateBWImageToggle(apiUri: string, value: boolean) {
|
|
||||||
return patchData(`${apiUri}/v2/Settings/BWImages`, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetAprilFoolsToggle(apiUri: string) : Promise<boolean> {
|
|
||||||
return getData(`${apiUri}/v2/Settings/AprilFoolsMode`).then((state) => state as unknown as boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async UpdateAprilFoolsToggle(apiUri: string, value: boolean) {
|
|
||||||
return patchData(`${apiUri}/v2/Settings/AprilFoolsMode`, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetChapterNamingScheme(apiUri: string) : Promise<string> {
|
|
||||||
return getData(`${apiUri}/v2/Settings/ChapterNamingScheme`).then((state) => state as unknown as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async UpdateChapterNamingScheme(apiUri: string, value: string) {
|
|
||||||
return patchData(`${apiUri}/v2/Settings/ChapterNamingScheme`, value);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import {getData} from "../../App";
|
|
||||||
import IChapter from "../types/IChapter";
|
|
||||||
|
|
||||||
export default class Chapter {
|
|
||||||
|
|
||||||
static async GetChapterFromId(apiUri: string, chapterId: string): Promise<IChapter> {
|
|
||||||
if(chapterId === undefined || chapterId === null) {
|
|
||||||
console.error(`chapterId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return getData(`${apiUri}/v2/Query/Chapter/${chapterId}`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info("Got all Manga");
|
|
||||||
const ret = json as IChapter;
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
import {Cookies} from "react-cookie";
|
|
||||||
import IFrontendSettings from "../types/IFrontendSettings";
|
|
||||||
|
|
||||||
export function LoadFrontendSettings(): IFrontendSettings {
|
|
||||||
const cookies = new Cookies();
|
|
||||||
return {
|
|
||||||
jobInterval: cookies.get('jobInterval') === undefined
|
|
||||||
? new Date(Date.parse("1970-01-01T03:00:00.000Z"))
|
|
||||||
: cookies.get('jobInterval'),
|
|
||||||
apiUri: cookies.get('apiUri') === undefined
|
|
||||||
? `${window.location.protocol}//${window.location.host}/api`
|
|
||||||
: cookies.get('apiUri')
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,208 +0,0 @@
|
|||||||
import {deleteData, getData, patchData, postData, putData} from '../../App';
|
|
||||||
import IJob, {JobState, JobType} from "../types/Jobs/IJob";
|
|
||||||
import IModifyJobRecord from "../types/records/IModifyJobRecord";
|
|
||||||
import IDownloadAvailableJobsRecord from "../types/records/IDownloadAvailableJobsRecord";
|
|
||||||
|
|
||||||
export default class Job
|
|
||||||
{
|
|
||||||
static IntervalStringFromDate(date: Date) : string {
|
|
||||||
let x = new Date(date);
|
|
||||||
return `${x.getDay()}.${x.getHours()}:${x.getMinutes()}:${x.getSeconds()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetAllJobs(apiUri: string): Promise<IJob[]> {
|
|
||||||
//console.info("Getting all Jobs");
|
|
||||||
return getData(`${apiUri}/v2/Job`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info("Got all Jobs");
|
|
||||||
const ret = json as IJob[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetJobsWithIds(apiUri: string, jobIds: string[]): Promise<IJob[]> {
|
|
||||||
return postData(`${apiUri}/v2/Job/WithIDs`, jobIds)
|
|
||||||
.then((json) => {
|
|
||||||
//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 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 Jobs");
|
|
||||||
const ret = json as IJob[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Jobs");
|
|
||||||
const ret = json as IJob[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetJob(apiUri: string, jobId: string): Promise<IJob>{
|
|
||||||
if(jobId === undefined || jobId === null || jobId.length < 1) {
|
|
||||||
console.error(`JobId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
//console.info(`Getting Job ${jobId}`);
|
|
||||||
return getData(`${apiUri}/v2/Job/${jobId}`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Job ${jobId}`);
|
|
||||||
const ret = json as IJob;
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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 async CreateDownloadAvailableChaptersJob(apiUri: string, mangaId: string, data: IDownloadAvailableJobsRecord): Promise<string[]> {
|
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
|
||||||
console.error(`mangaId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
if(data === undefined || data === null) {
|
|
||||||
console.error(`recurrenceMs was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return putData(`${apiUri}/v2/Job/DownloadAvailableChaptersJob/${mangaId}`, data)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Job ${jobId}`);
|
|
||||||
const ret = json as string[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async CreateDownloadSingleChapterJob(apiUri: string, chapterId: string): Promise<string[]> {
|
|
||||||
if(chapterId === undefined || chapterId === null || chapterId.length < 1) {
|
|
||||||
console.error(`mangaId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return putData(`${apiUri}/v2/Job/DownloadSingleChapterJob/${chapterId}`, {})
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Job ${jobId}`);
|
|
||||||
const ret = json as string[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async CreateUpdateFilesJob(apiUri: string, mangaId: string): Promise<string[]> {
|
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
|
||||||
console.error(`mangaId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return putData(`${apiUri}/v2/Job/UpdateFilesJob/${mangaId}`, {})
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Job ${jobId}`);
|
|
||||||
const ret = json as string[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async CreateUpdateAllFilesJob(apiUri: string): Promise<string[]> {
|
|
||||||
return putData(`${apiUri}/v2/Job/UpdateAllFilesJob`, {})
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Job ${jobId}`);
|
|
||||||
const ret = json as string[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async CreateUpdateMetadataJob(apiUri: string, mangaId: string): Promise<string[]> {
|
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
|
||||||
console.error(`mangaId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return putData(`${apiUri}/v2/Job/UpdateMetadataJob/${mangaId}`, {})
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Job ${jobId}`);
|
|
||||||
const ret = json as string[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async CreateUpdateAllMetadataJob(apiUri: string): Promise<string[]> {
|
|
||||||
return putData(`${apiUri}/v2/Job/UpdateAllMetadataJob`, {})
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Job ${jobId}`);
|
|
||||||
const ret = json as string[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static StartJob(apiUri: string, jobId: string) : Promise<object> {
|
|
||||||
return postData(`${apiUri}/v2/Job/${jobId}/Start`, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
static StopJob(apiUri: string, jobId: string) : Promise<object> {
|
|
||||||
return postData(`${apiUri}/v2/Job/${jobId}/Stop`, {});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
import {deleteData, getData, patchData, putData} from "../../App";
|
|
||||||
import INewLibraryRecord from "../types/records/INewLibraryRecord";
|
|
||||||
import ILocalLibrary from "../types/ILocalLibrary";
|
|
||||||
|
|
||||||
export default class LocalLibrary
|
|
||||||
{
|
|
||||||
static async GetLibraries(apiUri: string): Promise<ILocalLibrary[]> {
|
|
||||||
return getData(`${apiUri}/v2/LocalLibraries`)
|
|
||||||
.then((json) => {
|
|
||||||
const ret = json as ILocalLibrary[];
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetLibrary(apiUri: string, libraryId: string): Promise<ILocalLibrary> {
|
|
||||||
return getData(`${apiUri}/v2/LocalLibraries/${libraryId}`)
|
|
||||||
.then((json) => {
|
|
||||||
const ret = json as ILocalLibrary;
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async CreateLibrary(apiUri: string, data: INewLibraryRecord): Promise<ILocalLibrary> {
|
|
||||||
return putData(`${apiUri}/v2/LocalLibraries`, data)
|
|
||||||
.then((json) => {
|
|
||||||
const ret = json as ILocalLibrary;
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async DeleteLibrary(apiUri: string, libraryId: string): Promise<void> {
|
|
||||||
return deleteData(`${apiUri}/v2/LocalLibraries/${libraryId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async ChangeLibraryPath(apiUri: string, libraryId: string, newPath: string): Promise<void> {
|
|
||||||
return patchData(`${apiUri}/v2/LocalLibraries/${libraryId}/ChangeBasePath`, newPath)
|
|
||||||
.then(()=> {
|
|
||||||
return Promise.resolve()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async ChangeLibraryName(apiUri: string, libraryId: string, newName: string): Promise<void> {
|
|
||||||
return patchData(`${apiUri}/v2/LocalLibraries/${libraryId}/ChangeName`, newName)
|
|
||||||
.then(()=> {
|
|
||||||
return Promise.resolve()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async UpdateLibrary(apiUri: string, libraryId: string, record: INewLibraryRecord): Promise<void> {
|
|
||||||
return patchData(`${apiUri}/v2/LocalLibraries/${libraryId}`, record)
|
|
||||||
.then(()=> {
|
|
||||||
return Promise.resolve()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,156 +0,0 @@
|
|||||||
import {deleteData, getData, patchData, postData} from '../../App';
|
|
||||||
import IManga from "../types/IManga";
|
|
||||||
import IChapter from "../types/IChapter";
|
|
||||||
|
|
||||||
export default class Manga
|
|
||||||
{
|
|
||||||
static async GetAllManga(apiUri: string): Promise<IManga[]> {
|
|
||||||
//console.info("Getting all Manga");
|
|
||||||
return getData(`${apiUri}/v2/Manga`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info("Got all Manga");
|
|
||||||
const ret = json as IManga[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 postData(`${apiUri}/v2/Manga/WithIds`, mangaIds)
|
|
||||||
.then((json) => {
|
|
||||||
//console.debug(`Got Manga ${internalIds.join(",")}`);
|
|
||||||
const ret = json as IManga[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
if(ref == null || ref == undefined)
|
|
||||||
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=64&height=64`;
|
|
||||||
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetChapters(apiUri: string, mangaId: string): Promise<IChapter[]> {
|
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
|
||||||
console.error(`mangaId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return getData(`${apiUri}/v2/Manga/${mangaId}/Chapters`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Manga ${internalId}`);
|
|
||||||
const ret = json as IChapter[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetDownloadedChapters(apiUri: string, mangaId: string): Promise<IChapter[]> {
|
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
|
||||||
console.error(`mangaId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/Downloaded`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Manga ${internalId}`);
|
|
||||||
const ret = json as IChapter[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetNotDownloadedChapters(apiUri: string, mangaId: string): Promise<IChapter[]> {
|
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
|
||||||
console.error(`mangaId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/NotDownloaded`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Manga ${internalId}`);
|
|
||||||
const ret = json as IChapter[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetLatestChapterAvailable(apiUri: string, mangaId: string): Promise<IChapter> {
|
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
|
||||||
console.error(`mangaId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestAvailable`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Manga ${internalId}`);
|
|
||||||
const ret = json as IChapter;
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetLatestChapterDownloaded(apiUri: string, mangaId: string): Promise<IChapter> {
|
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
|
||||||
console.error(`mangaId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestDownloaded`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info(`Got Manga ${internalId}`);
|
|
||||||
const ret = json as IChapter;
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async SetIgnoreThreshold(apiUri: string, mangaId: string, chapterThreshold: number): Promise<object> {
|
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
|
||||||
console.error(`mangaId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
if(chapterThreshold === undefined || chapterThreshold === null) {
|
|
||||||
console.error(`chapterThreshold was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return patchData(`${apiUri}/v2/Manga/${mangaId}/IgnoreChaptersBefore`, chapterThreshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async MoveFolder(apiUri: string, mangaId: string, newPath: string): Promise<object> {
|
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1) {
|
|
||||||
console.error(`mangaId was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
if(newPath === undefined || newPath === null || newPath.length < 1) {
|
|
||||||
console.error(`newPath was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return postData(`${apiUri}/v2/Manga/{MangaId}/MoveFolder`, {newPath});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
import IMangaConnector from '../types/IMangaConnector';
|
|
||||||
import {getData, patchData} from '../../App';
|
|
||||||
|
|
||||||
export class MangaConnector
|
|
||||||
{
|
|
||||||
static async GetAllConnectors(apiUri: string): Promise<IMangaConnector[]> {
|
|
||||||
//console.info("Getting all MangaConnectors");
|
|
||||||
return getData(`${apiUri}/v2/MangaConnector`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info("Got all MangaConnectors");
|
|
||||||
return (json as IMangaConnector[]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetEnabledConnectors(apiUri: string): Promise<IMangaConnector[]> {
|
|
||||||
//console.info("Getting all enabled MangaConnectors");
|
|
||||||
return getData(`${apiUri}/v2/MangaConnector/enabled`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info("Got all enabled MangaConnectors");
|
|
||||||
return (json as IMangaConnector[]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async GetDisabledConnectors(apiUri: string): Promise<IMangaConnector[]> {
|
|
||||||
//console.info("Getting all disabled MangaConnectors");
|
|
||||||
return getData(`${apiUri}/v2/MangaConnector/disabled`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info("Got all disabled MangaConnectors");
|
|
||||||
return (json as IMangaConnector[]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async SetConnectorEnabled(apiUri: string, connectorName: string, enabled: boolean): Promise<object> {
|
|
||||||
if(connectorName === undefined || connectorName === null || connectorName.length < 1) {
|
|
||||||
console.error(`connectorName was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
if(enabled === undefined || enabled === null) {
|
|
||||||
console.error(`enabled was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return patchData(`${apiUri}/v2/MangaConnector/${connectorName}/SetEnabled/${enabled}`, {});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
import {deleteData, getData, putData} from "../../App";
|
|
||||||
import IGotifyRecord from "../types/records/IGotifyRecord";
|
|
||||||
import INtfyRecord from "../types/records/INtfyRecord";
|
|
||||||
import IPushoverRecord from "../types/records/IPushoverRecord";
|
|
||||||
import INotificationConnector from "../types/INotificationConnector";
|
|
||||||
|
|
||||||
export default class NotificationConnector {
|
|
||||||
|
|
||||||
static async GetNotificationConnectors(apiUri: string) : Promise<INotificationConnector[]> {
|
|
||||||
//console.info("Getting Notification Connectors");
|
|
||||||
return getData(`${apiUri}/v2/NotificationConnector`)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info("Got Notification Connectors");
|
|
||||||
const ret = json as INotificationConnector[];
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async CreateNotificationConnector(apiUri: string, newConnector: INotificationConnector): Promise<string> {
|
|
||||||
return putData(`${apiUri}/v2/NotificationConnector`, newConnector)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info("Got Notification Connectors");
|
|
||||||
const ret = json as unknown as string;
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
//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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
//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 CreatePushover(apiUri: string, pushover: IPushoverRecord) : Promise<string> {
|
|
||||||
if(pushover === undefined || pushover === null) {
|
|
||||||
console.error(`pushover was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
//console.info("Getting Notification Connectors");
|
|
||||||
return putData(`${apiUri}/v2/NotificationConnector/Pushover`, pushover)
|
|
||||||
.then((json) => {
|
|
||||||
//console.info("Got Notification Connectors");
|
|
||||||
const ret = json as unknown as string;
|
|
||||||
//console.debug(ret);
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
import {postData} from "../../App";
|
|
||||||
import IManga from "../types/IManga";
|
|
||||||
|
|
||||||
export default class SearchFunctions {
|
|
||||||
|
|
||||||
static async SearchName(apiUri: string, name: string) : Promise<IManga[]> {
|
|
||||||
if(name === undefined || name === null || name.length < 1) {
|
|
||||||
console.error(`name was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return postData(`${apiUri}/v2/Search/Name`, name)
|
|
||||||
.then((json) => {
|
|
||||||
const ret = json as IManga[];
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async SearchNameOnConnector(apiUri: string, connectorName: string, name: string) : Promise<IManga[]> {
|
|
||||||
if(connectorName === undefined || connectorName === null || connectorName.length < 1) {
|
|
||||||
console.error(`connectorName was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
if(name === undefined || name === null || name.length < 1) {
|
|
||||||
console.error(`name was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return postData(`${apiUri}/v2/Search/${connectorName}`, name)
|
|
||||||
.then((json) => {
|
|
||||||
const ret = json as IManga[];
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async SearchUrl(apiUri: string, url: string) : Promise<IManga> {
|
|
||||||
if(url === undefined || url === null || url.length < 1) {
|
|
||||||
console.error(`name was not provided`);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
return postData(`${apiUri}/v2/Search/Url`, url)
|
|
||||||
.then((json) => {
|
|
||||||
const ret = json as IManga;
|
|
||||||
return (ret);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export enum LibraryType {
|
|
||||||
Komga = "Komga",
|
|
||||||
Kavita = "Kavita"
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export enum MangaReleaseStatus {
|
|
||||||
Continuing = "Continuing",
|
|
||||||
Completed = "Completed",
|
|
||||||
OnHiatus = "OnHiatus",
|
|
||||||
Cancelled = "Cancelled",
|
|
||||||
Unreleased = "Unreleased",
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
export enum RequestLimitType {
|
|
||||||
Default = "Default",
|
|
||||||
MangaDexFeed = "MangaDexFeed",
|
|
||||||
MangaImage = "MangaImage",
|
|
||||||
MangaCover = "MangaCover",
|
|
||||||
MangaDexImage = "MangaDexImage",
|
|
||||||
MangaInfo = "MangaInfo"
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export default interface IAuthor {
|
|
||||||
authorId: string;
|
|
||||||
authorName: string;
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
export default interface IBackendSettings {
|
|
||||||
downloadLocation: string;
|
|
||||||
workingDirectory: string;
|
|
||||||
userAgent: string;
|
|
||||||
aprilFoolsMode: boolean;
|
|
||||||
requestLimits: {
|
|
||||||
Default: number,
|
|
||||||
MangaInfo: number,
|
|
||||||
MangaDexFeed: number,
|
|
||||||
MangaDexImage: number,
|
|
||||||
MangaImage: number,
|
|
||||||
MangaCover: number,
|
|
||||||
};
|
|
||||||
compression: number;
|
|
||||||
bwImages: boolean;
|
|
||||||
startNewJobTimeoutMs: number;
|
|
||||||
chapterNamingScheme: string;
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
export default interface IChapter{
|
|
||||||
chapterId: string;
|
|
||||||
volumeNumber: number;
|
|
||||||
chapterNumber: string;
|
|
||||||
url: string;
|
|
||||||
title: string | undefined;
|
|
||||||
archiveFileName: string;
|
|
||||||
downloaded: boolean;
|
|
||||||
parentMangaId: string;
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export default interface IFrontendSettings {
|
|
||||||
jobInterval: Date;
|
|
||||||
apiUri: string;
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import {LibraryType} from "./EnumLibraryType";
|
|
||||||
|
|
||||||
export default interface ILibraryConnector {
|
|
||||||
libraryConnectorId: string;
|
|
||||||
libraryType: LibraryType;
|
|
||||||
baseUrl: string;
|
|
||||||
auth: string;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export default interface ILink {
|
|
||||||
linkId: string;
|
|
||||||
linkProvider: string;
|
|
||||||
linkUrl: string;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export default interface ILocalLibrary {
|
|
||||||
localLibraryId: string;
|
|
||||||
basePath: string;
|
|
||||||
libraryName: string;
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import {MangaReleaseStatus} from "./EnumMangaReleaseStatus";
|
|
||||||
|
|
||||||
export default interface IManga{
|
|
||||||
mangaId: string;
|
|
||||||
idOnConnectorSite: 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[];
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export default interface IMangaAltTitle {
|
|
||||||
altTitleId: string;
|
|
||||||
language: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export default interface IMangaConnector {
|
|
||||||
name: string;
|
|
||||||
supportedLanguages: string[];
|
|
||||||
iconUrl: string;
|
|
||||||
baseUris: string[];
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export default interface INotificationConnector {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
headers: Record<string, string>[];
|
|
||||||
httpMethod: string;
|
|
||||||
body: string;
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
export default interface IRequestLimits {
|
|
||||||
Default: number;
|
|
||||||
MangaDexFeed: number;
|
|
||||||
MangaImage: number;
|
|
||||||
MangaCover: number;
|
|
||||||
MangaDexImage: number;
|
|
||||||
MangaInfo: number;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import IJob from "./IJob";
|
|
||||||
|
|
||||||
export default interface IDownloadAvailableChaptersJob extends IJob {
|
|
||||||
mangaId: string;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import IJob from "./IJob";
|
|
||||||
|
|
||||||
export default interface IDownloadMangaCoverJob extends IJob {
|
|
||||||
mangaId: string;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import IJob from "./IJob";
|
|
||||||
|
|
||||||
export default interface IDownloadSingleChapterJob extends IJob {
|
|
||||||
chapterId: string;
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
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",
|
|
||||||
MoveMangaLibraryJob = "MoveMangaLibraryJob"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum JobState {
|
|
||||||
Waiting = "Waiting",
|
|
||||||
Running = "Running",
|
|
||||||
Completed = "Completed",
|
|
||||||
Failed = "Failed"
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import IJob from "./IJob";
|
|
||||||
|
|
||||||
export default interface IMoveFileOrFolderJob extends IJob {
|
|
||||||
fromLocation: string;
|
|
||||||
toLocation: string;
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import IJob from "./IJob";
|
|
||||||
|
|
||||||
export default interface IMoveMangaLibraryJob extends IJob {
|
|
||||||
MangaId: string;
|
|
||||||
ToLibraryId: string;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import IJob from "./IJob";
|
|
||||||
|
|
||||||
export default interface IRetrieveChaptersJob extends IJob {
|
|
||||||
mangaId: string;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import IJob from "./IJob";
|
|
||||||
|
|
||||||
export default interface IUpdateFilesDownloadedJob extends IJob {
|
|
||||||
mangaId: string;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import IJob from "./IJob";
|
|
||||||
|
|
||||||
export default interface IUpdateMetadataJob extends IJob {
|
|
||||||
mangaId: string;
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export default interface IDownloadAvailableJobsRecord {
|
|
||||||
recurrenceTimeMs: number;
|
|
||||||
localLibraryId: string;
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import "../../../styles/notificationConnector.css";
|
|
||||||
import {isValidUri} from "../../../App";
|
|
||||||
|
|
||||||
export default interface IGotifyRecord {
|
|
||||||
endpoint: string;
|
|
||||||
appToken: string;
|
|
||||||
priority: number;
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import {ReactElement, useState} from "react";
|
|
||||||
import NotificationConnector from "../../api/NotificationConnector";
|
|
||||||
import Loader from "../../Loader";
|
|
||||||
import "../../../styles/notificationConnector.css";
|
|
||||||
|
|
||||||
export default interface ILunaseaRecord {
|
|
||||||
id: string;
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export default interface IModifyJobRecord {
|
|
||||||
recurrenceMs: number;
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
export default interface INewLibraryRecord {
|
|
||||||
path: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Validate(record: INewLibraryRecord) : boolean {
|
|
||||||
if(record.path.length < 1)
|
|
||||||
return false;
|
|
||||||
if(record.name.length < 1)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import "../../../styles/notificationConnector.css";
|
|
||||||
|
|
||||||
export default interface INtfyRecord {
|
|
||||||
endpoint: string;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
topic: string;
|
|
||||||
priority: number;
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import "../../../styles/notificationConnector.css";
|
|
||||||
|
|
||||||
export default interface IPushoverRecord {
|
|
||||||
apptoken: string;
|
|
||||||
user: string;
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
footer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
align-content: center;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
color: white;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
#madeWith {
|
|
||||||
flex-grow: 1;
|
|
||||||
text-align: right;
|
|
||||||
margin-right: 20px;
|
|
||||||
cursor: url("../media/blahaj.png"), grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer .statusBadge {
|
|
||||||
margin: 0 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
background-color: rgba(255,255,255, 0.3);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 2px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer > div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer .hoverHand {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: var(--topbar-height);
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
z-index: 100;
|
|
||||||
box-shadow: 0 0 20px black;
|
|
||||||
}
|
|
||||||
|
|
||||||
header > #titlebox {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
margin: 0 0 0 40px;
|
|
||||||
height: 100%;
|
|
||||||
align-items:center;
|
|
||||||
justify-content:center;
|
|
||||||
}
|
|
||||||
|
|
||||||
header > #titlebox > span{
|
|
||||||
cursor: default;
|
|
||||||
font-size: 24pt;
|
|
||||||
font-weight: bold;
|
|
||||||
background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header > #titlebox > img {
|
|
||||||
height: 100%;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
header > * {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
:root{
|
|
||||||
--background-color: #030304;
|
|
||||||
--second-background-color: white;
|
|
||||||
--primary-color: #f5a9b8;
|
|
||||||
--secondary-color: #5bcefa;
|
|
||||||
--blur-background: rgba(245, 169, 184, 0.58);
|
|
||||||
--accent-color: #fff;
|
|
||||||
/* --primary-color: green;
|
|
||||||
--secondary-color: gold;
|
|
||||||
--blur-background: rgba(86, 131, 36, 0.8);
|
|
||||||
--accent-color: olive; */
|
|
||||||
--topbar-height: 60px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
scrollbar-color: var(--primary-color) var(--second-background-color);
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
body{
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: var(--background-color);
|
|
||||||
font-family: "Inter", sans-serif;
|
|
||||||
overflow-x: hidden;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip:hover:before {
|
|
||||||
display: block;
|
|
||||||
content: attr(data-tooltip, "tooltip");
|
|
||||||
background-color: var(--second-background-color);
|
|
||||||
color: var(--secondary-color);
|
|
||||||
border: 1px solid var(--secondary-color);
|
|
||||||
border-radius: 6px;
|
|
||||||
bottom: 1em;
|
|
||||||
max-width: 90%;
|
|
||||||
position: absolute;
|
|
||||||
padding: 3px 7px 1px;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
span[is-loading="done"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
span[is-loading="loading"] {
|
|
||||||
transform: rotateZ(45deg);
|
|
||||||
perspective: 1000px;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
color: var(--second-background-color);
|
|
||||||
position: fixed;
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
background-blend-mode: lighten;
|
|
||||||
margin: 25vh calc(sin(70)*(50% - 40px));
|
|
||||||
}
|
|
||||||
|
|
||||||
span[is-loading="loading"]:before,
|
|
||||||
span[is-loading="loading"]:after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: inherit;
|
|
||||||
height: inherit;
|
|
||||||
border-radius: 50%;
|
|
||||||
transform: rotateX(70deg);
|
|
||||||
animation: 1s spin linear infinite;
|
|
||||||
}
|
|
||||||
span[is-loading="loading"]:after {
|
|
||||||
color: var(--primary-color);
|
|
||||||
transform: rotateY(70deg);
|
|
||||||
animation-delay: .4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotate {
|
|
||||||
0% {
|
|
||||||
transform: translate(-50%, -50%) rotateZ(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(-50%, -50%) rotateZ(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotateccw {
|
|
||||||
0% {
|
|
||||||
transform: translate(-50%, -50%) rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(-50%, -50%) rotate(-360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
box-shadow: .2em 0px 0 0px currentcolor;
|
|
||||||
}
|
|
||||||
12% {
|
|
||||||
box-shadow: .2em .2em 0 0 currentcolor;
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
box-shadow: 0 .2em 0 0px currentcolor;
|
|
||||||
}
|
|
||||||
37% {
|
|
||||||
box-shadow: -.2em .2em 0 0 currentcolor;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: -.2em 0 0 0 currentcolor;
|
|
||||||
}
|
|
||||||
62% {
|
|
||||||
box-shadow: -.2em -.2em 0 0 currentcolor;
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
box-shadow: 0px -.2em 0 0 currentcolor;
|
|
||||||
}
|
|
||||||
87% {
|
|
||||||
box-shadow: .2em -.2em 0 0 currentcolor;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
.LocalLibraryFunctions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.LocalLibraryFunctions input{
|
|
||||||
width: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.LocalLibraryFunctions-Action {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
@ -1,253 +0,0 @@
|
|||||||
.MangaItem{
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
width: 200px;
|
|
||||||
height: 300px;
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 200px 600px;
|
|
||||||
grid-template-rows: 80px 190px 30px;
|
|
||||||
grid-template-areas:
|
|
||||||
"cover tags"
|
|
||||||
"cover description"
|
|
||||||
"cover footer";
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem[is-clicked="clicked"]{
|
|
||||||
width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem::after{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0; top: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 100%; height: 100%;
|
|
||||||
background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem > * {
|
|
||||||
z-index: 2;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.startSearchEntry::after{
|
|
||||||
background: initial !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Connector {
|
|
||||||
grid-area: cover;
|
|
||||||
top: 10px;
|
|
||||||
left: 10px;
|
|
||||||
flex-grow: 0;
|
|
||||||
height: 14pt;
|
|
||||||
font-size: 12pt;
|
|
||||||
border-radius: 9pt;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
padding: 2pt 17px;
|
|
||||||
color: black;
|
|
||||||
width: fit-content;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Name{
|
|
||||||
grid-area: cover;
|
|
||||||
width: fit-content;
|
|
||||||
font-size: 16pt;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
top: 50px;
|
|
||||||
left: 10px;
|
|
||||||
margin: 0;
|
|
||||||
max-width: calc(100% - 20px);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Website {
|
|
||||||
grid-area: cover;
|
|
||||||
display: block;
|
|
||||||
height: 13px;
|
|
||||||
width: 13px;
|
|
||||||
margin: 0px 0px auto auto;
|
|
||||||
top: 12px;
|
|
||||||
right: 10px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Status {
|
|
||||||
grid-area: cover;
|
|
||||||
display:block;
|
|
||||||
height: 15px;
|
|
||||||
width: 15px;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 14px;
|
|
||||||
right: 35px;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 10px, rgb(51, 51, 51) 0px 0px 10px 3px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Status::after {
|
|
||||||
content: attr(release-status);
|
|
||||||
position: absolute;
|
|
||||||
top: -2.5pt;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
visibility: hidden;
|
|
||||||
|
|
||||||
/*Text Properties*/
|
|
||||||
font-size:10pt;
|
|
||||||
font-weight:bold;
|
|
||||||
color:white;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
/*Size*/
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 0px;
|
|
||||||
background-color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Status:hover::after{
|
|
||||||
visibility:visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Status[release-status="Continuing"]{
|
|
||||||
background-color: limegreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Status[release-status="Completed"]{
|
|
||||||
background-color: blueviolet;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Status[release-status="On Hiatus"]{
|
|
||||||
background-color: darkorange;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Status[release-status="Cancelled"]{
|
|
||||||
background-color: firebrick;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Status[release-status="Unreleased"]{
|
|
||||||
background-color: aqua;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Status[release-status="Status Unavailable"]{
|
|
||||||
background-color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Cover {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
grid-area: cover;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 5px;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem[is-clicked="not-clicked"] .MangaItem-Description, .MangaItem[is-clicked="not-clicked"] .MangaItem-Tags, .MangaItem[is-clicked="not-clicked"] .MangaItem-Props{
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem[is-clicked="clicked"] .MangaItem-Description, .MangaItem[is-clicked="clicked"] .MangaItem-Tags, .MangaItem[is-clicked="clicked"] .MangaItem-Props{
|
|
||||||
display: block;
|
|
||||||
width: calc(100% - 6px);
|
|
||||||
background-color: white;
|
|
||||||
padding: 0 3px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Tags {
|
|
||||||
grid-area: tags;
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
border-bottom: 1px solid var(--secondary-color);
|
|
||||||
justify-content: start;
|
|
||||||
align-content: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Tags > * {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: max-content;
|
|
||||||
margin: 1px 2px !important;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: white;
|
|
||||||
padding: 3px 2px 4px 2px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Tags > * > * {
|
|
||||||
margin: 0 2px !important;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Tags > * > svg {
|
|
||||||
margin: auto 2px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Tags a, .MangaItem-Tags a:visited {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Author {
|
|
||||||
background-color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Tag {
|
|
||||||
background-color: blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Link{
|
|
||||||
background-color: brown;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Description {
|
|
||||||
grid-area: description;
|
|
||||||
color: black;
|
|
||||||
max-height: 40vh;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
border-bottom: 1px solid var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Props {
|
|
||||||
grid-area: footer;
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Props > * {
|
|
||||||
margin: 3px;
|
|
||||||
border: 1px solid var(--primary-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: transparent;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Props-Threshold {
|
|
||||||
color: black;
|
|
||||||
padding: 0 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Props-Threshold > input {
|
|
||||||
margin: 0 3px;
|
|
||||||
width: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem-Props-Threshold-Available{
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
#MonitorMangaList {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaActionButtons {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaActionButtons > div {
|
|
||||||
position: absolute;
|
|
||||||
margin: 10px;
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.DeleteJobButton {
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
filter: invert(21%) sepia(63%) saturate(7443%) hue-rotate(355deg) brightness(93%) contrast(118%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.StartJobNowButton {
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
filter: invert(58%) sepia(16%) saturate(4393%) hue-rotate(103deg) brightness(102%) contrast(103%);
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
.NotificationConnectorItem{
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 40% calc(60% - 10px);
|
|
||||||
grid-template-rows: 30px auto auto 30px;
|
|
||||||
column-gap: 4px;
|
|
||||||
row-gap: 4px;
|
|
||||||
grid-template-areas:
|
|
||||||
"name name"
|
|
||||||
"url explanation "
|
|
||||||
"headers body"
|
|
||||||
"footer footer";
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotificationConnectorItem p{
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotificationConnectorItem-Name{
|
|
||||||
grid-area: name;
|
|
||||||
justify-self: flex-start;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotificationConnectorItem-Name::before {
|
|
||||||
content: "Connector-Name";
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotificationConnectorItem-Url{
|
|
||||||
grid-area: url;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotificationConnectorItem-Body{
|
|
||||||
grid-area: body;
|
|
||||||
width: calc(100% - 2px);
|
|
||||||
height: max-content;
|
|
||||||
min-height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotificationConnectorItem-Explanation{
|
|
||||||
grid-area: explanation;
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotificationConnectorItem-Priority {
|
|
||||||
grid-area: explanation;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotificationConnectorItem-Ident {
|
|
||||||
grid-area: body;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotificationConnectorItem-Headers{
|
|
||||||
grid-area: headers;
|
|
||||||
justify-self: flex-end;
|
|
||||||
align-self: flex-end;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.NotificationConnectorItem-Header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotificationConnectorItem-Header > input {
|
|
||||||
width: 48%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotificationConnectorItem-Save{
|
|
||||||
grid-area: footer;
|
|
||||||
justify-self: flex-end;
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
.popup {
|
|
||||||
position: fixed;
|
|
||||||
left: 10%;
|
|
||||||
top: 7.5%;
|
|
||||||
width: 80%;
|
|
||||||
height: 80%;
|
|
||||||
margin: auto;
|
|
||||||
z-index: 100;
|
|
||||||
background-color: var(--second-background-color);
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup .popupHeader {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 40px;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup .popupHeader h1 {
|
|
||||||
margin: 4px 10px;
|
|
||||||
font-size: 20pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup .close {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup .popupBody {
|
|
||||||
position: absolute;
|
|
||||||
top: 40px;
|
|
||||||
left: 0;
|
|
||||||
width: calc(100% - 30px);
|
|
||||||
height: calc(100% - 50px);
|
|
||||||
padding: 5px 15px;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
#QueuePopUp #QueuePopUpBody {
|
|
||||||
display: flex;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
#QueuePopUp #QueuePopUpBody > * {
|
|
||||||
padding: 20px;
|
|
||||||
width: calc(50% - 40px);
|
|
||||||
height: calc(100% - 40px);
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
#QueuePopUp #QueuePopUpBody h1 {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 0 5px 0;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#QueuePopUp #QueuePopUpBody > *:first-child {
|
|
||||||
border-right: 1px solid var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#QueuePopUp #QueuePopUpBody .JobQueue {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.QueueJob {
|
|
||||||
color: black;
|
|
||||||
margin: 5px 0;
|
|
||||||
position: relative;
|
|
||||||
height: 200px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 150px auto;
|
|
||||||
grid-template-rows: 25% 20% auto 15% 12%;
|
|
||||||
column-gap: 10px;
|
|
||||||
grid-template-areas:
|
|
||||||
"cover name"
|
|
||||||
"cover jobType"
|
|
||||||
"cover additionalInfo"
|
|
||||||
"cover progress"
|
|
||||||
"cover actions"
|
|
||||||
}
|
|
||||||
|
|
||||||
.QueueJob p {
|
|
||||||
margin: 2px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.QueueJob img{
|
|
||||||
grid-area: cover;
|
|
||||||
height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.QueueJob .QueueJob-Name{
|
|
||||||
grid-area: name;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.QueueJob .JobType{
|
|
||||||
grid-area: jobType;
|
|
||||||
}
|
|
||||||
|
|
||||||
.QueueJob .QueueJob-additionalInfo {
|
|
||||||
grid-area: additionalInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
.QueueJob .QueueJob-actions {
|
|
||||||
grid-area: actions;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
|
|
||||||
.QueueJob .QueueJob-Cancel {
|
|
||||||
grid-area: actions;
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.QueueJob .QueueJob-Progressbar {
|
|
||||||
grid-area: progress;
|
|
||||||
}
|
|