Compare commits
66 Commits
master
...
vite-react
Author | SHA1 | Date | |
---|---|---|---|
0402f9e6d0 | |||
1f8cacb668 | |||
e58d3f8741 | |||
7eca06332a | |||
363d4d7518 | |||
4660f9a402 | |||
07dfee15eb | |||
6b3e2899f0 | |||
251ec0a978 | |||
a486b5bdfc | |||
f426a77f25 | |||
24417ae180 | |||
3cd64b9bfb | |||
c525957b2e | |||
46ea39245e | |||
d621422ae3 | |||
f4011a7cbc | |||
a383ded819 | |||
7364a20d5d | |||
f3a091f09d | |||
fc3fa627c1 | |||
5e4e64f019 | |||
0a0e901ef7 | |||
746ad1c16c | |||
67bed57ab6 | |||
d97eff9796 | |||
d2533ee98f | |||
fcc1ff392c | |||
1304bc750a | |||
4aff0ed5e0 | |||
dbd6684faf | |||
73a1725eb9 | |||
efdcba57ae | |||
5cb638d9c1 | |||
eef819d140 | |||
d04e0f6374 | |||
ca3aa2e8e8 | |||
442b2ce0cc | |||
004f96194f | |||
c1d333e002 | |||
96b5163486 | |||
035d384411 | |||
59dff529ef | |||
5aac05ae08 | |||
db61837457 | |||
756998847c | |||
ad6502d824 | |||
f3cb3f209c | |||
da002df3f2 | |||
68887d65a7 | |||
6b08123ee5 | |||
adf876c37f | |||
d67b1754f9 | |||
09be1c64a3 | |||
30f3903662 | |||
3f26d3bbd6 | |||
daa05a0b4d | |||
2f9eb61377 | |||
65814dd942 | |||
dbad993c7a | |||
ac8ca1f886 | |||
f27f11e7c2 | |||
9d8dadc634 | |||
ca091bac92 | |||
cf09bc50fb | |||
d5115809ca |
@ -22,7 +22,7 @@ jobs:
|
|||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3.7.1
|
uses: docker/setup-buildx-action@v3.6.1
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push Website
|
- name: Build and push Website
|
||||||
uses: docker/build-push-action@v6.9.0
|
uses: docker/build-push-action@v6.7.0
|
||||||
with:
|
with:
|
||||||
context: ./Website
|
context: ./Website
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
4
.github/workflows/docker-image-dev.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3.7.1
|
uses: docker/setup-buildx-action@v3.6.1
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push Website
|
- name: Build and push Website
|
||||||
uses: docker/build-push-action@v6.9.0
|
uses: docker/build-push-action@v6.7.0
|
||||||
with:
|
with:
|
||||||
context: ./Website
|
context: ./Website
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
6
.github/workflows/docker-image-master.yml
vendored
@ -3,6 +3,8 @@ name: Docker Image CI
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -22,7 +24,7 @@ jobs:
|
|||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3.7.1
|
uses: docker/setup-buildx-action@v3.6.1
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -33,7 +35,7 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push Website
|
- name: Build and push Website
|
||||||
uses: docker/build-push-action@v6.9.0
|
uses: docker/build-push-action@v6.7.0
|
||||||
with:
|
with:
|
||||||
context: ./Website
|
context: ./Website
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
2
.gitignore
vendored
@ -24,3 +24,5 @@ cover.png
|
|||||||
.vs/tranga-website/FileContentIndex/91a465d3-1190-42e0-95eb-fa3694744e58.vsidx
|
.vs/tranga-website/FileContentIndex/91a465d3-1190-42e0-95eb-fa3694744e58.vsidx
|
||||||
.vs/tranga-website/v17/.wsuo
|
.vs/tranga-website/v17/.wsuo
|
||||||
.vs/VSWorkspaceState.json
|
.vs/VSWorkspaceState.json
|
||||||
|
/node_modules/
|
||||||
|
/.vite/
|
||||||
|
14
README.md
@ -57,7 +57,9 @@ This repo makes HTTP-requests to the [Tranga-API](https://github.com/C9Glax/tran
|
|||||||
### Built With
|
### Built With
|
||||||
|
|
||||||
- nginx
|
- nginx
|
||||||
- HTML, CSS, and barebones Javascript
|
- vite
|
||||||
|
- react
|
||||||
|
- typescript
|
||||||
- 💙 Blåhaj 🦈
|
- 💙 Blåhaj 🦈
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
@ -73,16 +75,6 @@ There is a single release:
|
|||||||
Download [docker-compose.yaml](https://github.com/C9Glax/tranga-website/blob/cuttingedge/docker-compose.yaml) and configure to your needs.
|
Download [docker-compose.yaml](https://github.com/C9Glax/tranga-website/blob/cuttingedge/docker-compose.yaml) and configure to your needs.
|
||||||
The `docker-compose` also includes [Tranga](https://github.com/C9Glax/tranga) as backend. For its configuration refer to the repo README.
|
The `docker-compose` also includes [Tranga](https://github.com/C9Glax/tranga) as backend. For its configuration refer to the repo README.
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
- [ ] ❓
|
|
||||||
|
|
||||||
See the [open issues](https://github.com/C9Glax/tranga-website/issues) for a full list of proposed features (and known issues).
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- CONTRIBUTING -->
|
<!-- CONTRIBUTING -->
|
||||||
## Contributing
|
## Contributing
|
||||||
|
Before Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 356 KiB |
Before Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 1.6 MiB |
132
Website/App.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
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 IFrontendSettings, {LoadFrontendSettings} from "./modules/interfaces/IFrontendSettings";
|
||||||
|
import {useCookies} from "react-cookie";
|
||||||
|
|
||||||
|
export default function App(){
|
||||||
|
const [, setCookie] = useCookies(['apiUri', 'jobInterval']);
|
||||||
|
const [connected, setConnected] = React.useState(false);
|
||||||
|
const [showSearch, setShowSearch] = React.useState(false);
|
||||||
|
const [frontendSettings, setFrontendSettings] = useState<IFrontendSettings>(LoadFrontendSettings());
|
||||||
|
const [updateInterval, setUpdateInterval] = React.useState<number>();
|
||||||
|
const [updateMonitorList, setUpdateMonitorList] = React.useState<Date>(new Date());
|
||||||
|
|
||||||
|
const apiUri = frontendSettings.apiUri;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkConnection(apiUri).then(res => setConnected(res)).catch(() => setConnected(false));
|
||||||
|
if(updateInterval === undefined){
|
||||||
|
setUpdateInterval(setInterval(() => {
|
||||||
|
checkConnection(apiUri).then(res => setConnected(res)).catch(() => setConnected(false));
|
||||||
|
}, 500));
|
||||||
|
}else{
|
||||||
|
clearInterval(updateInterval);
|
||||||
|
setUpdateInterval(undefined);
|
||||||
|
}
|
||||||
|
}, [frontendSettings]);
|
||||||
|
|
||||||
|
function ChangeSettings(settings: IFrontendSettings) {
|
||||||
|
setFrontendSettings(settings);
|
||||||
|
setCookie('apiUri', settings.apiUri);
|
||||||
|
setCookie('jobInterval', settings.jobInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateList = () => {setUpdateMonitorList(new Date())}
|
||||||
|
|
||||||
|
return(<div>
|
||||||
|
<Header apiUri={apiUri} backendConnected={connected} settings={frontendSettings} changeSettings={ChangeSettings}/>
|
||||||
|
{connected
|
||||||
|
? <>
|
||||||
|
{showSearch
|
||||||
|
? <>
|
||||||
|
<Search apiUri={apiUri} jobInterval={frontendSettings.jobInterval} onJobsChanged={UpdateList} closeSearch={() => setShowSearch(false)} />
|
||||||
|
<hr/>
|
||||||
|
</>
|
||||||
|
: <></>}
|
||||||
|
<MonitorJobsList updateList={updateMonitorList} apiUri={apiUri} onStartSearch={() => setShowSearch(true)} onJobsChanged={UpdateList} connectedToBackend={connected} />
|
||||||
|
</>
|
||||||
|
: <>
|
||||||
|
<h1>No connection to the Backend.</h1>
|
||||||
|
<h3>Check the Settings ApiUri.</h3>
|
||||||
|
</>}
|
||||||
|
<Footer apiUri={apiUri} connectedToBackend={connected} />
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getData(uri: string) : Promise<object> {
|
||||||
|
return fetch(uri,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers : {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function(response){
|
||||||
|
if(!response.ok) throw new Error("Could not fetch data");
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.catch(function(err){
|
||||||
|
console.error(`Error GETting Data ${uri}\n${err}`);
|
||||||
|
return Promise.reject();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postData(uri: string, content: object) : Promise<object> {
|
||||||
|
return fetch(uri,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers : {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(content)
|
||||||
|
})
|
||||||
|
.then(function(response){
|
||||||
|
if(!response.ok)
|
||||||
|
throw new Error("Could not fetch data");
|
||||||
|
let json = response.json();
|
||||||
|
return json.then((json) => json).catch(() => null);
|
||||||
|
})
|
||||||
|
.catch(function(err){
|
||||||
|
console.error(`Error POSTing Data ${uri}\n${err}`);
|
||||||
|
return Promise.reject();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteData(uri: string) : Promise<void> {
|
||||||
|
return fetch(uri,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers : {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() =>{
|
||||||
|
return Promise.resolve();
|
||||||
|
})
|
||||||
|
.catch(function(err){
|
||||||
|
console.error(`Error DELETEing Data ${uri}\n${err}`);
|
||||||
|
return Promise.reject();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidUri(uri: string) : boolean{
|
||||||
|
try {
|
||||||
|
new URL(uri);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
||||||
|
return getData(`${apiUri}/v2/Ping`).then((result) => {
|
||||||
|
return result != null;
|
||||||
|
}).catch(() => Promise.reject());
|
||||||
|
}
|
@ -1,346 +0,0 @@
|
|||||||
let apiUri = `${window.location.protocol}//${window.location.host.split(':')[0]}:6531`
|
|
||||||
|
|
||||||
if(getCookie("apiUri") != ""){
|
|
||||||
apiUri = getCookie("apiUri");
|
|
||||||
}
|
|
||||||
setCookie("apiUri", apiUri);
|
|
||||||
|
|
||||||
function setCookie(cname, cvalue) {
|
|
||||||
const d = new Date();
|
|
||||||
d.setTime(d.getTime() + (365*24*60*60*1000));
|
|
||||||
let expires = "expires="+ d.toUTCString();
|
|
||||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/;samesite=strict";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCookie(cname) {
|
|
||||||
let name = cname + "=";
|
|
||||||
let decodedCookie = decodeURIComponent(document.cookie);
|
|
||||||
let ca = decodedCookie.split(';');
|
|
||||||
for(let i = 0; i < ca.length; i++) {
|
|
||||||
let c = ca[i];
|
|
||||||
while (c.charAt(0) == ' ') {
|
|
||||||
c = c.substring(1);
|
|
||||||
}
|
|
||||||
if (c.indexOf(name) == 0) {
|
|
||||||
return c.substring(name.length, c.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetData(uri){
|
|
||||||
let request = await fetch(uri, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let json = await request.json();
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function PostData(uri){
|
|
||||||
let request = await fetch(uri, {
|
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
//console.log(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteData(uri){
|
|
||||||
fetch(uri, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function Ping(){
|
|
||||||
let ret = await GetData(`${apiUri}/Ping`);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetAvailableControllers(){
|
|
||||||
var uri = apiUri + "/Connectors";
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetPublicationFromConnector(connector, title){
|
|
||||||
var uri;
|
|
||||||
if(title.includes("http")){
|
|
||||||
uri = `${apiUri}/Manga/FromConnector?connector=${connector}&url=${title}`;
|
|
||||||
}else{
|
|
||||||
uri = `${apiUri}/Manga/FromConnector?connector=${connector}&title=${title}`;
|
|
||||||
}
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetChapters(connector, internalId, language){
|
|
||||||
var uri = `${apiUri}/Manga/Chapters?connector=${connector}&internalId=${internalId}&translatedLanguage=${language}`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
function GetCoverUrl(internalId){
|
|
||||||
return `${apiUri}/Manga/Cover?internalId=${internalId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetAllJobs(){
|
|
||||||
var uri = `${apiUri}/Jobs`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetRunningJobs(){
|
|
||||||
var uri = `${apiUri}/Jobs/Running`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetWaitingJobs(){
|
|
||||||
var uri = `${apiUri}/Jobs/Waiting`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetMonitorJobs(){
|
|
||||||
var uri = `${apiUri}/Jobs/MonitorJobs`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetProgress(jobId){
|
|
||||||
var uri = `${apiUri}/Jobs/Progress?jobId=${jobId}`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetSettings(){
|
|
||||||
var uri = `${apiUri}/Settings`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetAvailableNotificationConnectors(){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Types`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetNotificationConnectors(){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetAvailableLibraryConnectors(){
|
|
||||||
var uri = `${apiUri}/LibraryConnectors/Types`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetLibraryConnectors(){
|
|
||||||
var uri = `${apiUri}/LibraryConnectors`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetRateLimits() {
|
|
||||||
var uri = `${apiUri}/Settings/customRequestLimit`
|
|
||||||
let json = await GetData(uri);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateMonitorJob(connector, internalId, language){
|
|
||||||
var uri = `${apiUri}/Jobs/MonitorManga?connector=${connector}&internalId=${internalId}&interval=03:00:00&translatedLanguage=${language}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateDownloadNewChaptersJob(connector, internalId, language){
|
|
||||||
var uri = `${apiUri}/Jobs/DownloadNewChapters?connector=${connector}&internalId=${internalId}&translatedLanguage=${language}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StartJob(jobId){
|
|
||||||
var uri = `${apiUri}/Jobs/StartNow?jobId=${jobId}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateDownloadLocation(downloadLocation){
|
|
||||||
var uri = `${apiUri}/Settings/UpdateDownloadLocation?downloadLocation=${downloadLocation}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RefreshLibraryMetadata() {
|
|
||||||
var uri = `${apiUri}/Jobs/UpdateMetadata`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function DownloadLogs() {
|
|
||||||
var uri = `${apiUri}/LogFile`;
|
|
||||||
|
|
||||||
//Below taken from https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
|
|
||||||
fetch(uri)
|
|
||||||
.then((response) => response.body)
|
|
||||||
.then((rb) => {
|
|
||||||
const reader = rb.getReader();
|
|
||||||
|
|
||||||
return new ReadableStream({
|
|
||||||
start(controller) {
|
|
||||||
// The following function handles each data chunk
|
|
||||||
function push() {
|
|
||||||
// "done" is a Boolean and value a "Uint8Array"
|
|
||||||
reader.read().then(({ done, value }) => {
|
|
||||||
// If there is no more data to read
|
|
||||||
if (done) {
|
|
||||||
console.log("done", done);
|
|
||||||
controller.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Get the data and send it to the browser via the controller
|
|
||||||
controller.enqueue(value);
|
|
||||||
// Check chunks by logging to the console
|
|
||||||
console.log(done, value);
|
|
||||||
push();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
push();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((stream) =>
|
|
||||||
// Respond with our stream
|
|
||||||
new Response(stream, { headers: { "Content-Type": "text/html" } }).text(),
|
|
||||||
)
|
|
||||||
.then((result) => {
|
|
||||||
// Do things with result
|
|
||||||
//console.log(result);
|
|
||||||
|
|
||||||
//Below download taken from https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server
|
|
||||||
var element = document.createElement('a');
|
|
||||||
element.setAttribute('href', 'data:text/plain;charset-utf-8,' + encodeURIComponent(result));
|
|
||||||
var newDate = new Date();
|
|
||||||
var filename = "Tranga_Logs_" + newDate.today() + "_" + newDate.timeNow() + ".log";
|
|
||||||
element.setAttribute('download', filename);
|
|
||||||
element.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//Following date-time code taken from: https://stackoverflow.com/questions/10211145/getting-current-date-and-time-in-javascript
|
|
||||||
// For todays date;
|
|
||||||
Date.prototype.today = function () {
|
|
||||||
return ((this.getDate() < 10)?"0":"") + this.getDate() +"/"+(((this.getMonth()+1) < 10)?"0":"") + (this.getMonth()+1) +"/"+ this.getFullYear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the time now
|
|
||||||
Date.prototype.timeNow = function () {
|
|
||||||
return ((this.getHours() < 10)?"0":"") + this.getHours() +"_"+ ((this.getMinutes() < 10)?"0":"") + this.getMinutes() +"_"+ ((this.getSeconds() < 10)?"0":"") + this.getSeconds();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Komga
|
|
||||||
function UpdateKomga(komgaUrl, komgaAuth){
|
|
||||||
var uri = `${apiUri}/LibraryConnectors/Update?libraryConnector=Komga&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResetKomga(){
|
|
||||||
var uri = `${apiUri}/LibraryConnectors/Reset?libraryConnector=Komga`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TestKomga(komgaUrl, komgaAuth){
|
|
||||||
var uri = `${apiUri}/LibraryConnectors/Test?libraryConnector=Komga&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//Kavita
|
|
||||||
function UpdateKavita(kavitaUrl, kavitaUsername, kavitaPassword){
|
|
||||||
var uri = `${apiUri}/LibraryConnectors/Update?libraryConnector=Kavita&kavitaUrl=${kavitaUrl}&kavitaUsername=${kavitaUsername}&kavitaPassword=${kavitaPassword}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResetKavita(){
|
|
||||||
var uri = `${apiUri}/LibraryConnectors/Reset?libraryConnector=Kavita`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TestKavita(kavitaUrl, kavitaUsername, kavitaPassword){
|
|
||||||
var uri = `${apiUri}/LibraryConnectors/Test?libraryConnector=Kavita&kavitaUrl=${kavitaUrl}&kavitaUsername=${kavitaUsername}&kavitaPassword=${kavitaPassword}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Gotify
|
|
||||||
function UpdateGotify(gotifyUrl, gotifyAppToken){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Update?notificationConnector=Gotify&gotifyUrl=${gotifyUrl}&gotifyAppToken=${gotifyAppToken}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResetGotify(){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Reset?notificationConnector=Gotify`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TestGotify(gotifyUrl, gotifyAppToken){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Test?notificationConnector=Gotify&gotifyUrl=${gotifyUrl}&gotifyAppToken=${gotifyAppToken}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
//LunaSea
|
|
||||||
function UpdateLunaSea(lunaseaWebhook){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Update?notificationConnector=LunaSea&lunaseaWebhook=${lunaseaWebhook}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResetLunaSea(){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Reset?notificationConnector=LunaSea`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TestLunaSea(lunaseaWebhook){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Test?notificationConnector=LunaSea&lunaseaWebhook=${lunaseaWebhook}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Ntfy
|
|
||||||
function UpdateNtfy(ntfyEndpoint, ntfyAuth){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Update?notificationConnector=Ntfy&ntfyUrl=${ntfyEndpoint}&ntfyAuth=${ntfyAuth}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResetNtfy(){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Reset?notificationConnector=Ntfy`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TestNtfy(ntfyEndpoint, ntfyAuth){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Test?notificationConnector=Ntfy&ntfyUrl=${ntfyEndpoint}&ntfyAuth=${ntfyAuth}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateUserAgent(userAgent){
|
|
||||||
var uri = `${apiUri}/Settings/userAgent?userAgent=${userAgent}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateRateLimit(byteValue, rateLimit) {
|
|
||||||
var uri = `${apiUri}/Settings/customRequestLimit?requestType=${byteValue}&requestsPerMinute=${rateLimit}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RemoveJob(jobId){
|
|
||||||
var uri = `${apiUri}/Jobs?jobId=${jobId}`;
|
|
||||||
DeleteData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CancelJob(jobId){
|
|
||||||
var uri = `${apiUri}/Jobs/Cancel?jobId=${jobId}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetLogmessages(count){
|
|
||||||
var uri = `${apiUri}/LogMessages?count=${count}`;
|
|
||||||
let json = await GetData(uri);
|
|
||||||
console.log(json);
|
|
||||||
return json;
|
|
||||||
}
|
|
@ -1,301 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Tranga</title>
|
<title>Tranga</title>
|
||||||
<link id='basestyle' rel="stylesheet" href="styles/base.css">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<link id='librarystyle' rel="stylesheet" href="styles/style_default.css">
|
<link rel="stylesheet" href="styles/index.css">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<wrapper>
|
<div id="app"></div>
|
||||||
|
<script type="module" src="index.jsx"></script>
|
||||||
<topbar>
|
|
||||||
<titlebox>
|
|
||||||
<img alt="website image is Blahaj" src="media/blahaj.png">
|
|
||||||
<span>Tranga</span>
|
|
||||||
</titlebox>
|
|
||||||
<spacer></spacer>
|
|
||||||
<img id="filterFunnel" src="media/filter-funnel.svg" height="50%" alt="filterFunnel">
|
|
||||||
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
|
|
||||||
</topbar>
|
|
||||||
|
|
||||||
<filter-box id="filterBox">
|
|
||||||
<border-bar>
|
|
||||||
<popup-title>Filter by: </popup-title>
|
|
||||||
<popup-close onclick="filterBox.classList.toggle('animate')" >×</popup-close>
|
|
||||||
</border-bar>
|
|
||||||
<popup-content id="filterContent">
|
|
||||||
<div class="popup-section">
|
|
||||||
NAME:
|
|
||||||
<div class="section-content">
|
|
||||||
<label for="searchbox"></label><input id="searchbox" placeholder="Title" type="text">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class = "popup-section">
|
|
||||||
CONNECTOR:
|
|
||||||
<div class="section-content" id="connectorFilterBox">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class = "popup-section">
|
|
||||||
STATUS:
|
|
||||||
<div class="section-content" id="statusFilterBox">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</popup-content>
|
|
||||||
<border-bar-button onclick="ClearFilter()" class="clearFilter">Clear Filter</border-bar-button>
|
|
||||||
</filter-box>
|
|
||||||
|
|
||||||
|
|
||||||
<viewport>
|
|
||||||
<div id="loaderdiv">
|
|
||||||
<blur-background></blur-background>
|
|
||||||
<div id="loader"></div>
|
|
||||||
<p id="loaderText">Check your Settings > API-URI</p>
|
|
||||||
</div>
|
|
||||||
<content>
|
|
||||||
<div id="addPublication">
|
|
||||||
<p>+</p>
|
|
||||||
</div>
|
|
||||||
<publication onclick="ShowNewMangaSearch()">
|
|
||||||
<img alt="cover" src="media/cover.jpg">
|
|
||||||
<publication-information>
|
|
||||||
<connector-name class="pill">Sample</connector-name>
|
|
||||||
<publication-name>Best Manga there is</publication-name>
|
|
||||||
</publication-information>
|
|
||||||
</publication>
|
|
||||||
</content>
|
|
||||||
|
|
||||||
<popup id="newMangaPopup">
|
|
||||||
<blur-background id="blurBackgroundNewMangaPopup" onclick="newMangaPopup.style.display = 'none';"></blur-background>
|
|
||||||
<div id="newMangaPopupSelector">
|
|
||||||
<select id="newMangaConnector" />
|
|
||||||
<input type="text" placeholder="Title" id="newMangaTitle" />
|
|
||||||
<select id="newMangaTranslatedLanguage">
|
|
||||||
<option selected="selected">en</option>
|
|
||||||
<option>it</option>
|
|
||||||
<option>de</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div id="newMangaResult"></div>
|
|
||||||
</popup>
|
|
||||||
|
|
||||||
<popup id="settingsPopup">
|
|
||||||
<blur-background id="blurBackgroundSettingsPopup" onclick="settingsPopup.style.display = 'none';"></blur-background>
|
|
||||||
<popup-window>
|
|
||||||
<border-bar>
|
|
||||||
<popup-title>Settings</popup-title>
|
|
||||||
<popup-close onclick="settingsPopup.style.display = 'none'">×</popup-close>
|
|
||||||
</border-bar>
|
|
||||||
<popup-content>
|
|
||||||
|
|
||||||
<div class="popup-section">
|
|
||||||
TRANGA
|
|
||||||
<div class="section-content">
|
|
||||||
<div class="section-item dyn-height">
|
|
||||||
<span class="title">API Settings</span>
|
|
||||||
<row><label for="settingApiUri">API URI:</label><input placeholder="https://" type="text" id="settingApiUri"></row>
|
|
||||||
<row><label for="userAgent">User Agent:</label><input placeholder="UserAgent" id="userAgent" type="text"></row>
|
|
||||||
<row>
|
|
||||||
<border-bar-button class="section" onclick="RefreshLibraryMetadata()">Refresh Library Metadata</border-bar-button>
|
|
||||||
<border-bar-button class="section" onclick="DownloadLogs()">Download Logs</border-bar-button>
|
|
||||||
</row>
|
|
||||||
</div>
|
|
||||||
<div class="section-item dyn-height">
|
|
||||||
<span class="title">Rate Limits</span>
|
|
||||||
<row><label for="DefaultRL">Default:</label><input id="defaultRL" type="text" ></row>
|
|
||||||
<row><label for="CoverRL">Manga Covers:</label><input id="coverRL" type="text"></row>
|
|
||||||
<row><label for="ImageRL">Manga Images:</label><input id="imageRL" type="text"></row>
|
|
||||||
<row><label for="InfoRL">Manga Info:</label><input id="infoRL" type="text"></row>
|
|
||||||
</div>
|
|
||||||
<div class="section-item dyn-height">
|
|
||||||
<span class="title">Appearance</span>
|
|
||||||
<row><label for="cssStyle">Library Style:</label><select id="cssStyle">
|
|
||||||
<option id="card_compact" value="card_compact">Cards (Compact)</option>
|
|
||||||
<option id="card_hover" value="card_hover">Cards (Hover)</option>
|
|
||||||
</select></row>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="popup-section">
|
|
||||||
MANGA SOURCES
|
|
||||||
<div class="section-content">
|
|
||||||
<!-- <div class="section-item dyn-height">
|
|
||||||
<span class="title"><img src="connector-icons/manganato.png"><a href="https://manganato.com">MangaNato</a></span>
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
<!-- <div class="section-item dyn-height">
|
|
||||||
<span class="title"><img src="connector-icons/mangasee.png"><a href="https://mangasee123.com">MangaSee</a></span>
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
<div class="section-item dyn-height">
|
|
||||||
<span class="title"><img src="connector-icons/mangadex-logo.svg"><a href="https://mangadex.org">MangaDex</a></span>
|
|
||||||
<row><label for="mDexAuthorRL">Author Rate Limit:</label><input id="mDexAuthorRL" type="text"></row>
|
|
||||||
<row><label for="mDexFeedRL">Feed Rate Limit:</label><input id="mDexFeedRL" type="text"></row>
|
|
||||||
<row><label for="mDexImageRL">Image Rate Limit:</label><input id="mDexImageRL" type="text"></row>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="section-item dyn-height">
|
|
||||||
<span class="title"><img src="connector-icons/mangakatana.png"><a href="https://mangakatana.com">MangaKatana</a></span>
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
<!-- <div class="section-item dyn-height">
|
|
||||||
<span class="title"><img src="connector-icons/mangaworld.png"><a href="https://www.mangaworld.ac">MangaWorld</a></span>
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
<!-- <div class="section-item dyn-height">
|
|
||||||
<span class="title"><img src="connector-icons/bato.ico"><a href="https://bato.to">Bato</a></span>
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
<!-- <div class="section-item dyn-height">
|
|
||||||
<span class="title"><img src="connector-icons/mangalife.png"><a href="https://www.manga4life.com">MangaLife</a></span>
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="popup-section">
|
|
||||||
LIBRARY CONNECTORS
|
|
||||||
<div class="section-content">
|
|
||||||
<div class="section-item">
|
|
||||||
<span class="title"><img src='connector-icons/komga.svg'>Komga<connector-configured id="komgaConfigured"></connector-configured></span>
|
|
||||||
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
|
|
||||||
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
|
|
||||||
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
|
|
||||||
<div class="section-buttons-container">
|
|
||||||
<span onclick="TestKomga(komgaUrl.value, utf8_to_b64(`${komgaUsername.value}:${komgaPassword.value}`))" class='section-button' id="test-connector">Test</span>
|
|
||||||
<span onclick="ClearKomga()" class='section-button' id="reset">Reset</span>
|
|
||||||
<span onclick="UpdateKomga(komgaUrl.value, utf8_to_b64(`${komgaUsername.value}:${komgaPassword.value}`))" class='section-button'>Apply</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-item">
|
|
||||||
<span class="title"><img src='connector-icons/kavita.png'>Kavita<connector-configured id="kavitaConfigured"></connector-configured></span>
|
|
||||||
<label for="kavitaUrl"></label><input placeholder="URL" id="kavitaUrl" type="text">
|
|
||||||
<label for="kavitaUsername"></label><input placeholder="Username" id="kavitaUsername" type="text">
|
|
||||||
<label for="kavitaPassword"></label><input placeholder="Password" id="kavitaPassword" type="password">
|
|
||||||
<div class="section-buttons-container">
|
|
||||||
<span onclick="TestKavita(kavitaUrl.value, kavitaUsername.value, kavitaPassword.value)" class='section-button' id="test-connector">Test</span>
|
|
||||||
<span onclick="ClearKavita()" class='section-button' id="reset">Reset</span>
|
|
||||||
<span onclick="UpdateKavita(kavitaUrl.value, kavitaUsername.value, kavitaPassword.value)" class='section-button'>Apply</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="popup-section">
|
|
||||||
NOTIFICATION CONNECTORS
|
|
||||||
<div class="section-content">
|
|
||||||
<div class="section-item">
|
|
||||||
<span class="title"><img src='connector-icons/gotify-logo.png'>Gotify<connector-configured id="gotifyConfigured"></connector-configured></span>
|
|
||||||
<label for="gotifyUrl"></label><input placeholder="URL" id="gotifyUrl" type="text">
|
|
||||||
<label for="gotifyAppToken"></label><input placeholder="App-Token" id="gotifyAppToken" type="text">
|
|
||||||
<div class="section-buttons-container">
|
|
||||||
<span onclick="TestGotify(gotifyUrl.value, gotifyAppToken.value)" class='section-button' id="test-connector">Test</span>
|
|
||||||
<span onclick="ClearGotify()" class='section-button' id="reset">Reset</span>
|
|
||||||
<span onclick="UpdateGotify(gotifyUrl.value, gotifyAppToken.value)" class='section-button'>Apply</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-item">
|
|
||||||
<span class="title"><img src='connector-icons/lunasea.png'>LunaSea<connector-configured id="lunaseaConfigured"></connector-configured></span>
|
|
||||||
<label for="lunaseaWebhook"></label><input placeholder="device/:id or user/:id" id="lunaseaWebhook" type="text">
|
|
||||||
<div class="section-buttons-container">
|
|
||||||
<span onclick="TestLunaSea(lunaseaWebhook.value);" class='section-button' id="test-connector">Test</span>
|
|
||||||
<span onclick="ClearLunasea()" class='section-button' id="reset">Reset</span>
|
|
||||||
<span onclick="UpdateLunaSea(lunaseaWebhook.value);" class='section-button'>Apply</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-item">
|
|
||||||
<span class="title"><img src='connector-icons/ntfy.svg'>Ntfy<connector-configured id="ntfyConfigured"></connector-configured></span>
|
|
||||||
<label for="ntfyEndpoint"></label><input placeholder="URL" id="ntfyEndpoint" type="text">
|
|
||||||
<label for="ntfyAuth"></label><input placeholder="Auth" id="ntfyAuth" type="text">
|
|
||||||
<div class="section-buttons-container">
|
|
||||||
<span onclick="TestNtfy(ntfyEndpoint.value, ntfyAuth.value);" class='section-button' id="test-connector">Test</span>
|
|
||||||
<span onclick="ClearNtfy()" class='section-button' id="reset">Reset</span>
|
|
||||||
<span onclick="UpdateNtfy(ntfyEndpoint.value, ntfyAuth.value);" class='section-button'>Apply</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</popup-content>
|
|
||||||
|
|
||||||
<border-bar>
|
|
||||||
<div class="button-container">
|
|
||||||
<border-bar-button class="primary" onclick="UpdateSettings()">Apply Settings</border-bar-button>
|
|
||||||
</div>
|
|
||||||
</border-bar>
|
|
||||||
|
|
||||||
</popup-window>
|
|
||||||
</popup>
|
|
||||||
|
|
||||||
<popup id="publicationViewerPopup">
|
|
||||||
<blur-background id="blurBackgroundPublicationPopup" onclick="publicationViewerPopup.style.display= 'none';"></blur-background>
|
|
||||||
<publication-viewer>
|
|
||||||
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
|
|
||||||
<publication-details>
|
|
||||||
<publication-name id="publicationViewerName">Best Manga there is</publication-name>
|
|
||||||
<publication-tags id="publicationViewerTags">A Manga</publication-tags>
|
|
||||||
<publication-author id="publicationViewerAuthor">Glax</publication-author>
|
|
||||||
<publication-description id="publicationViewerDescription">
|
|
||||||
An interesting description. The description is very intriguing, yet wholesome.
|
|
||||||
</publication-description>
|
|
||||||
<publication-interactions>
|
|
||||||
<publication-starttask id="startJobButton">Start Job ▶️</publication-starttask>
|
|
||||||
<publication-canceltask id="cancelJobButton">Cancel Job ❌</publication-canceltask>
|
|
||||||
<publication-delete id="deleteJobButton">Delete Job 🗑️</publication-delete>
|
|
||||||
<publication-add id="createMonitorJobButton">Monitor ➕</publication-add>
|
|
||||||
<publication-add id="createDownloadChapterJobButton">Download Chapter 📥</publication-add>
|
|
||||||
</publication-interactions>
|
|
||||||
</publication-details>
|
|
||||||
</publication-viewer>
|
|
||||||
</popup>
|
|
||||||
|
|
||||||
<popup id="jobStatusView">
|
|
||||||
<blur-background id="blurBackgroundSettingsPopup" onclick="jobStatusView.style.display = 'none';"></blur-background>
|
|
||||||
<popup-window>
|
|
||||||
<border-bar>
|
|
||||||
<popup-title>Jobs</popup-title>
|
|
||||||
<popup-close onclick="jobStatusView.style.display = 'none'">×</popup-close>
|
|
||||||
</border-bar>
|
|
||||||
<popup-content>
|
|
||||||
|
|
||||||
<div class="popup-section">
|
|
||||||
RUNNING JOBS
|
|
||||||
<div class="section-content" id="jobStatusRunning">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="popup-section">
|
|
||||||
QUEUED JOBS
|
|
||||||
<div class="section-content" id="jobStatusWaiting">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</popup-content>
|
|
||||||
|
|
||||||
<border-bar>
|
|
||||||
<!-- <div class="button-container">
|
|
||||||
<border-bar-button class="primary" onclick="UpdateSettings()">Apply Settings</border-bar-button>
|
|
||||||
</div> -->
|
|
||||||
</border-bar>
|
|
||||||
|
|
||||||
</popup-window>
|
|
||||||
</viewport>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div onclick="ShowJobQueue();">
|
|
||||||
<img src="media/running.svg" alt="running"><div id="jobsRunningTag">0</div>
|
|
||||||
</div>
|
|
||||||
<div onclick="ShowJobQueue();">
|
|
||||||
<img src="media/queue.svg" alt="queue"><div id="jobsQueuedTag">0</div>
|
|
||||||
</div>
|
|
||||||
<p id="madeWith">Made with Blåhaj 🦈</p>
|
|
||||||
</footer>
|
|
||||||
</wrapper>
|
|
||||||
|
|
||||||
<script src="apiConnector.js"></script>
|
|
||||||
<script src="interaction.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
7
Website/index.jsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
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 />);
|
@ -1,885 +0,0 @@
|
|||||||
let monitoringJobsCount = 0;
|
|
||||||
let runningJobs = [];
|
|
||||||
let waitingJobs = [];
|
|
||||||
let notificationConnectorTypes = [];
|
|
||||||
let libraryConnectorTypes = [];
|
|
||||||
let selectedManga;
|
|
||||||
let selectedJob;
|
|
||||||
let searchMatch;
|
|
||||||
|
|
||||||
let connectorMatch = [];
|
|
||||||
let connectorNameMatch;
|
|
||||||
let statusMatch = [];
|
|
||||||
let statusNameMatch = [];
|
|
||||||
|
|
||||||
const searchBox = document.querySelector("#searchbox");
|
|
||||||
const settingsPopup = document.querySelector("#settingsPopup");
|
|
||||||
const filterBox = document.querySelector("#filterBox");
|
|
||||||
const settingsCog = document.querySelector("#settingscog");
|
|
||||||
const filterFunnel = document.querySelector("#filterFunnel");
|
|
||||||
const tasksContent = document.querySelector("content");
|
|
||||||
const createMonitorTaskButton = document.querySelector("#createMonitoJobButton");
|
|
||||||
const createDownloadChapterTaskButton = document.querySelector("#createDownloadChapterJobButton");
|
|
||||||
const startJobButton = document.querySelector("#startJobButton");
|
|
||||||
const cancelJobButton = document.querySelector("#cancelJobButton");
|
|
||||||
const deleteJobButton = document.querySelector("#deleteJobButton");
|
|
||||||
|
|
||||||
//Manga viewer popup
|
|
||||||
const mangaViewerPopup = document.querySelector("#publicationViewerPopup");
|
|
||||||
const mangaViewerWindow = document.querySelector("publication-viewer");
|
|
||||||
const mangaViewerDescription = document.querySelector("#publicationViewerDescription");
|
|
||||||
const mangaViewerName = document.querySelector("#publicationViewerName");
|
|
||||||
const mangaViewerTags = document.querySelector("#publicationViewerTags");
|
|
||||||
const mangaViewerAuthor = document.querySelector("#publicationViewerAuthor");
|
|
||||||
const mangaViewCover = document.querySelector("#pubviewcover");
|
|
||||||
|
|
||||||
//General Rate Limits
|
|
||||||
const defaultRL = document.querySelector("#defaultRL");
|
|
||||||
const coverRL = document.querySelector("#coverRL");
|
|
||||||
const imageRL = document.querySelector("#imageRL");
|
|
||||||
const infoRL = document.querySelector("#infoRL");
|
|
||||||
|
|
||||||
//MangaDex Rate Limits
|
|
||||||
const mDexAuthorRL = document.querySelector("#mDexAuthorRL");
|
|
||||||
const mDexFeedRL = document.querySelector("#mDexFeedRL");
|
|
||||||
const mDexImageRL = document.querySelector("#mDexImageRL");
|
|
||||||
|
|
||||||
//Komga
|
|
||||||
const settingKomgaUrl = document.querySelector("#komgaUrl");
|
|
||||||
const settingKomgaUser = document.querySelector("#komgaUsername");
|
|
||||||
const settingKomgaPass = document.querySelector("#komgaPassword");
|
|
||||||
|
|
||||||
//Kavita
|
|
||||||
const settingKavitaUrl = document.querySelector("#kavitaUrl");
|
|
||||||
const settingKavitaUser = document.querySelector("#kavitaUsername");
|
|
||||||
const settingKavitaPass = document.querySelector("#kavitaPassword");
|
|
||||||
|
|
||||||
//Gotify
|
|
||||||
const settingGotifyUrl = document.querySelector("#gotifyUrl");
|
|
||||||
const settingGotifyAppToken = document.querySelector("#gotifyAppToken");
|
|
||||||
|
|
||||||
//Lunasea
|
|
||||||
const settingLunaseaWebhook = document.querySelector("#lunaseaWebhook");
|
|
||||||
|
|
||||||
//Ntfy
|
|
||||||
const settingNtfyEndpoint = document.querySelector("#ntfyEndpoint");
|
|
||||||
const settingNtfyAuth = document.querySelector("#ntfyAuth");
|
|
||||||
|
|
||||||
//Connector Configured
|
|
||||||
const settingKomgaConfigured = document.querySelector("#komgaConfigured");
|
|
||||||
const settingKavitaConfigured = document.querySelector("#kavitaConfigured");
|
|
||||||
const settingGotifyConfigured = document.querySelector("#gotifyConfigured");
|
|
||||||
const settingLunaseaConfigured = document.querySelector("#lunaseaConfigured");
|
|
||||||
const settingNtfyConfigured = document.querySelector("#ntfyConfigured");
|
|
||||||
|
|
||||||
const settingUserAgent = document.querySelector("#userAgent");
|
|
||||||
const settingApiUri = document.querySelector("#settingApiUri");
|
|
||||||
const settingCSSStyle = document.querySelector('#cssStyle');
|
|
||||||
const newMangaPopup = document.querySelector("#newMangaPopup");
|
|
||||||
const newMangaConnector = document.querySelector("#newMangaConnector");
|
|
||||||
const newMangaTitle = document.querySelector("#newMangaTitle");
|
|
||||||
const newMangaResult = document.querySelector("#newMangaResult");
|
|
||||||
const newMangaTranslatedLanguage = document.querySelector("#newMangaTranslatedLanguage");
|
|
||||||
|
|
||||||
//Jobs
|
|
||||||
const jobsRunningTag = document.querySelector("#jobsRunningTag");
|
|
||||||
const jobsQueuedTag = document.querySelector("#jobsQueuedTag");
|
|
||||||
const loaderdiv = document.querySelector('#loaderdiv');
|
|
||||||
const jobStatusView = document.querySelector("#jobStatusView");
|
|
||||||
const jobStatusRunning = document.querySelector("#jobStatusRunning");
|
|
||||||
const jobStatusWaiting = document.querySelector("#jobStatusWaiting");
|
|
||||||
|
|
||||||
function Setup(){
|
|
||||||
Ping().then((ret) => {
|
|
||||||
loaderdiv.style.display = 'none';
|
|
||||||
|
|
||||||
GetAvailableNotificationConnectors().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
json.forEach(connector => {
|
|
||||||
notificationConnectorTypes[connector.Key] = connector.Value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
GetAvailableLibraryConnectors().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
json.forEach(connector => {
|
|
||||||
libraryConnectorTypes[connector.Key] = connector.Value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
GetAvailableControllers().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
newMangaConnector.replaceChildren();
|
|
||||||
connectorFilterBox = document.querySelector("#connectorFilterBox");
|
|
||||||
connectorFilterBox.replaceChildren();
|
|
||||||
json.forEach(connector => {
|
|
||||||
//Add the connector to the New Manga dropdown
|
|
||||||
var option = document.createElement('option');
|
|
||||||
option.value = connector;
|
|
||||||
option.innerText = connector;
|
|
||||||
newMangaConnector.appendChild(option);
|
|
||||||
|
|
||||||
//Add the connector to the filter box
|
|
||||||
connectorFilter = document.createElement('connector-name');
|
|
||||||
connectorFilter.innerText = connector;
|
|
||||||
connectorFilter.className = "pill";
|
|
||||||
connectorFilter.style.backgroundColor = stringToColour(connector);
|
|
||||||
|
|
||||||
connectorFilter.addEventListener("click", (event) => {
|
|
||||||
ToggleFilterConnector(connector, event);
|
|
||||||
});
|
|
||||||
connectorFilterBox.appendChild(connectorFilter);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//Add the publication status options to the filter bar
|
|
||||||
publicationStatusOptions = ["Ongoing", "Completed", "On Hiatus", "Cancelled", "Upcoming", "Status Unavailable"];
|
|
||||||
statusFilterBox = document.querySelector("#statusFilterBox");
|
|
||||||
statusFilterBox.replaceChildren();
|
|
||||||
publicationStatusOptions.forEach(publicationStatus => {
|
|
||||||
var releaseStatus = document.createElement('status-filter');
|
|
||||||
releaseStatus.innerText = publicationStatus;
|
|
||||||
releaseStatus.setAttribute("release-status", publicationStatus);
|
|
||||||
releaseStatus.addEventListener("click", (event) => {
|
|
||||||
ToggleFilterStatus(publicationStatus, event);
|
|
||||||
});
|
|
||||||
|
|
||||||
statusFilterBox.appendChild(releaseStatus);
|
|
||||||
});
|
|
||||||
|
|
||||||
ResetContent();
|
|
||||||
UpdateJobs();
|
|
||||||
GetSettings().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
settingApiUri.placeholder = apiUri;
|
|
||||||
});
|
|
||||||
GetRateLimits().then((json) => {
|
|
||||||
defaultRL.placeholder = json.Default + ' Requests/Minute';
|
|
||||||
coverRL.placeholder = json.MangaCover + ' Requests/Minute';
|
|
||||||
imageRL.placeholder = json.MangaImage + ' Requests/Minute';
|
|
||||||
infoRL.placeholder = json.MangaInfo + ' Requests/Minute';
|
|
||||||
mDexAuthorRL.placeholder = json.MangaDexAuthor + ' Requests/Minute';
|
|
||||||
mDexFeedRL.placeholder = json.MangaDexFeed + ' Requests/Minute';
|
|
||||||
mDexImageRL.placeholder = json.MangaDexImage + ' Requests/Minute';
|
|
||||||
});
|
|
||||||
|
|
||||||
//If the cssStyle key isn't in the local storage of the browser, then set the css style to the default and load the page
|
|
||||||
//Otherwise get the style key from storage and set it.
|
|
||||||
if (!localStorage.getItem('cssStyle')) {
|
|
||||||
localStorage.setItem('cssStyle', 'card_compact');
|
|
||||||
document.getElementById('librarystyle').setAttribute('href', 'styles/' + localStorage.getItem('cssStyle') + '.css');
|
|
||||||
document.getElementById('card_compact').selected = true;
|
|
||||||
} else {
|
|
||||||
css_style = localStorage.getItem('cssStyle');
|
|
||||||
document.getElementById('librarystyle').setAttribute('href', 'styles/' + css_style + '.css');
|
|
||||||
document.getElementById(css_style).selected = true;
|
|
||||||
}
|
|
||||||
setInterval(() => {
|
|
||||||
UpdateJobs();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
//Clear the previous values if any exist.
|
|
||||||
searchBox.value = "";
|
|
||||||
connectorMatch.length = 0;
|
|
||||||
statusMatch.length = 0;
|
|
||||||
}
|
|
||||||
Setup();
|
|
||||||
|
|
||||||
function ToggleFilterConnector(connector, event) {
|
|
||||||
//console.log("Initial Array:");
|
|
||||||
//console.log(connectorMatch);
|
|
||||||
if (connectorMatch.includes(connector)) {
|
|
||||||
idx = connectorMatch.indexOf(connector);
|
|
||||||
connectorMatch.splice(idx, 1);
|
|
||||||
event.target.style.outline = 'none';
|
|
||||||
event.target.style.outlineOffset = "0px";
|
|
||||||
} else {
|
|
||||||
connectorMatch.push(connector);
|
|
||||||
event.target.style.outline = '4px solid var(--secondary-color)';
|
|
||||||
event.target.style.outlineOffset = '3px';
|
|
||||||
}
|
|
||||||
//console.log("Final Array");
|
|
||||||
//console.log(connectorMatch);
|
|
||||||
FilterResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToggleFilterStatus(status, event) {
|
|
||||||
//console.log("Initial Array:");
|
|
||||||
//console.log(statusMatch);
|
|
||||||
if (statusMatch.includes(status)) {
|
|
||||||
idx = statusMatch.indexOf(status);
|
|
||||||
statusMatch.splice(idx, 1);
|
|
||||||
event.target.style.outline = 'none';
|
|
||||||
event.target.style.outlineOffset = "0px";
|
|
||||||
} else {
|
|
||||||
statusMatch.push(status);
|
|
||||||
event.target.style.outline = '4px solid var(--secondary-color)';
|
|
||||||
event.target.style.outlineOffset = '3px';
|
|
||||||
}
|
|
||||||
//console.log("Final Array");
|
|
||||||
//console.log(statusMatch);
|
|
||||||
FilterResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClearFilter() {
|
|
||||||
searchBox.value = "";
|
|
||||||
statusMatch.length = 0;
|
|
||||||
connectorMatch.length = 0;
|
|
||||||
FilterResults();
|
|
||||||
|
|
||||||
//Get rid of the outlines
|
|
||||||
connectorFilterBox = document.querySelector("#connectorFilterBox");
|
|
||||||
connectorFilterBox.childNodes.forEach(connector => {
|
|
||||||
if (connector.nodeName.toLowerCase() == 'connector-name') {
|
|
||||||
connector.style.outline = 'none';
|
|
||||||
connector.style.outlineOffset = "0px";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
statusFilterBox = document.querySelector("#statusFilterBox");
|
|
||||||
statusFilterBox.childNodes.forEach(publicationStatus => {
|
|
||||||
if (publicationStatus.nodeName.toLowerCase() == 'status-filter') {
|
|
||||||
publicationStatus.style.outline = 'none';
|
|
||||||
publicationStatus.style.outlineOffset = "0px";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
settingCSSStyle.addEventListener("change", (event) => {
|
|
||||||
localStorage.setItem('cssStyle', settingCSSStyle.value);
|
|
||||||
document.getElementById('librarystyle').setAttribute('href', 'styles/' + localStorage.getItem('cssStyle') + '.css');
|
|
||||||
});
|
|
||||||
|
|
||||||
function ResetContent(){
|
|
||||||
//Delete everything
|
|
||||||
tasksContent.replaceChildren();
|
|
||||||
|
|
||||||
//Add "Add new Task" Button
|
|
||||||
var add = document.createElement("div");
|
|
||||||
add.setAttribute("id", "addPublication")
|
|
||||||
var plus = document.createElement("p");
|
|
||||||
plus.innerText = "+";
|
|
||||||
add.appendChild(plus);
|
|
||||||
add.addEventListener("click", () => ShowNewMangaSearch());
|
|
||||||
tasksContent.appendChild(add);
|
|
||||||
|
|
||||||
//Populate with the monitored mangas
|
|
||||||
GetMonitorJobs().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
json.forEach(job => {
|
|
||||||
var mangaView = CreateManga(job.manga, job.mangaConnector.name);
|
|
||||||
mangaView.addEventListener("click", (event) => {
|
|
||||||
ShowMangaWindow(job, job.manga, event, false);
|
|
||||||
});
|
|
||||||
tasksContent.appendChild(mangaView);
|
|
||||||
});
|
|
||||||
monitoringJobsCount = json.length;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShowNewMangaSearch(){
|
|
||||||
newMangaTitle.value = "";
|
|
||||||
newMangaPopup.style.display = "block";
|
|
||||||
newMangaResult.replaceChildren();
|
|
||||||
}
|
|
||||||
|
|
||||||
newMangaTitle.addEventListener("keypress", (event) => { if(event.key === "Enter") GetNewMangaItems();});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function GetNewMangaItems(){
|
|
||||||
if(newMangaTitle.value.length < 4)
|
|
||||||
return;
|
|
||||||
|
|
||||||
newMangaResult.replaceChildren();
|
|
||||||
newMangaConnector.disabled = true;
|
|
||||||
newMangaTitle.disabled = true;
|
|
||||||
newMangaTranslatedLanguage.disabled = true;
|
|
||||||
GetPublicationFromConnector(newMangaConnector.value, newMangaTitle.value).then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
if(json.length > 0)
|
|
||||||
newMangaResult.style.display = "flex";
|
|
||||||
json.forEach(result => {
|
|
||||||
var mangaElement = CreateManga(result, newMangaConnector.value)
|
|
||||||
newMangaResult.appendChild(mangaElement);
|
|
||||||
mangaElement.addEventListener("click", (event) => {
|
|
||||||
ShowMangaWindow(null, result, event, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
newMangaConnector.disabled = false;
|
|
||||||
newMangaTitle.disabled = false;
|
|
||||||
newMangaTranslatedLanguage.disabled = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//Returns a new "Publication" Item to display in the jobs section
|
|
||||||
function CreateManga(manga, connector){
|
|
||||||
//Create a new publication and set an internal ID
|
|
||||||
var mangaElement = document.createElement('publication');
|
|
||||||
mangaElement.id = GetValidSelector(manga.internalId);
|
|
||||||
|
|
||||||
//Append the cover image to the publication
|
|
||||||
var mangaImage = document.createElement('img');
|
|
||||||
mangaImage.src = GetCoverUrl(manga.internalId);
|
|
||||||
mangaElement.appendChild(mangaImage);
|
|
||||||
|
|
||||||
//Append the publication information to the publication
|
|
||||||
//console.log(manga);
|
|
||||||
var info = document.createElement('publication-information');
|
|
||||||
var connectorName = document.createElement('connector-name');
|
|
||||||
connectorName.innerText = connector;
|
|
||||||
connectorName.className = "pill";
|
|
||||||
connectorName.style.backgroundColor = stringToColour(connector);
|
|
||||||
info.appendChild(connectorName);
|
|
||||||
|
|
||||||
var mangaName = document.createElement('publication-name');
|
|
||||||
mangaName.innerText = manga.sortName;
|
|
||||||
|
|
||||||
//Create the publication status indicator
|
|
||||||
var releaseStatus = document.createElement('publication-status');
|
|
||||||
releaseStatus.setAttribute("release-status", manga.releaseStatus);
|
|
||||||
switch(manga.releaseStatus){
|
|
||||||
case 0:
|
|
||||||
releaseStatus.setAttribute("release-status", "Ongoing");
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
releaseStatus.setAttribute("release-status", "Completed");
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
releaseStatus.setAttribute("release-status", "On Hiatus");
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
releaseStatus.setAttribute("release-status", "Cancelled");
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
releaseStatus.setAttribute("release-status", "Upcoming");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
releaseStatus.setAttribute("release-status", "Status Unavailable");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
info.appendChild(mangaName);
|
|
||||||
mangaElement.appendChild(info);
|
|
||||||
mangaElement.appendChild(releaseStatus); //Append the release status indicator to the publication element
|
|
||||||
return mangaElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
createMonitorJobButton.addEventListener("click", () => {
|
|
||||||
CreateMonitorJob(newMangaConnector.value, selectedManga.internalId, newMangaTranslatedLanguage.value);
|
|
||||||
UpdateJobs();
|
|
||||||
mangaViewerPopup.style.display = "none";
|
|
||||||
});
|
|
||||||
startJobButton.addEventListener("click", () => {
|
|
||||||
StartJob(selectedJob.id);
|
|
||||||
mangaViewerPopup.style.display = "none";
|
|
||||||
});
|
|
||||||
cancelJobButton.addEventListener("click", () => {
|
|
||||||
CancelJob(selectedJob.id);
|
|
||||||
mangaViewerPopup.style.display = "none";
|
|
||||||
});
|
|
||||||
deleteJobButton.addEventListener("click", () => {
|
|
||||||
RemoveJob(selectedJob.id);
|
|
||||||
UpdateJobs();
|
|
||||||
mangaViewerPopup.style.display = "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
function ShowMangaWindow(job, manga, event, add){
|
|
||||||
selectedManga = manga;
|
|
||||||
selectedJob = job;
|
|
||||||
//Show popup
|
|
||||||
mangaViewerPopup.style.display = "block";
|
|
||||||
|
|
||||||
//Set position to mouse-position
|
|
||||||
if(event.clientY < window.innerHeight - mangaViewerWindow.offsetHeight)
|
|
||||||
mangaViewerWindow.style.top = `${event.clientY}px`;
|
|
||||||
else
|
|
||||||
mangaViewerWindow.style.top = `${event.clientY - mangaViewerWindow.offsetHeight}px`;
|
|
||||||
|
|
||||||
if(event.clientX < window.innerWidth - mangaViewerWindow.offsetWidth)
|
|
||||||
mangaViewerWindow.style.left = `${event.clientX}px`;
|
|
||||||
else
|
|
||||||
mangaViewerWindow.style.left = `${event.clientX - mangaViewerWindow.offsetWidth}px`;
|
|
||||||
|
|
||||||
//Edit information inside the window
|
|
||||||
mangaViewerName.innerText = manga.sortName;
|
|
||||||
mangaViewerTags.innerText = manga.tags.join(", ");
|
|
||||||
mangaViewerDescription.innerText = manga.description;
|
|
||||||
mangaViewerAuthor.innerText = manga.authors.join(',');
|
|
||||||
mangaViewCover.src = GetCoverUrl(manga.internalId);
|
|
||||||
toEditId = manga.internalId;
|
|
||||||
|
|
||||||
//Check what action should be listed
|
|
||||||
if(add){
|
|
||||||
createMonitorJobButton.style.display = "initial";
|
|
||||||
createDownloadChapterJobButton.style.display = "none";
|
|
||||||
cancelJobButton.style.display = "none";
|
|
||||||
startJobButton.style.display = "none";
|
|
||||||
deleteJobButton.style.display = "none";
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
createMonitorJobButton.style.display = "none";
|
|
||||||
createDownloadChapterJobButton.style.display = "none";
|
|
||||||
cancelJobButton.style.display = "initial";
|
|
||||||
startJobButton.style.display = "initial";
|
|
||||||
deleteJobButton.style.display = "initial";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function HidePublicationPopup(){
|
|
||||||
publicationViewerPopup.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
searchBox.addEventListener("keyup", () => FilterResults());
|
|
||||||
//Filter shown jobs
|
|
||||||
function FilterResults(){
|
|
||||||
//For each publication
|
|
||||||
tasksContent.childNodes.forEach(publication => {
|
|
||||||
//If the search box isn't empty check that the title contains the searchbox content. If it does then
|
|
||||||
//'searchMatch' is true and the manga is shown. If the search box is empty, then consider this field
|
|
||||||
//to be true anyways.
|
|
||||||
if (searchBox.value.length > 0) {
|
|
||||||
publication.childNodes.forEach(item => {
|
|
||||||
if (item.nodeName.toLowerCase() == "publication-information"){
|
|
||||||
item.childNodes.forEach(information => {
|
|
||||||
if (information.nodeName.toLowerCase() == "publication-name") {
|
|
||||||
if (information.textContent.toLowerCase().includes(searchBox.value.toLowerCase())){
|
|
||||||
searchMatch = 1;
|
|
||||||
} else {
|
|
||||||
searchMatch = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
searchMatch = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
//If the array connectorMatch isn't empty then check that the connector matches one of the ones
|
|
||||||
//in the array
|
|
||||||
if (connectorMatch.length > 0) {
|
|
||||||
publication.childNodes.forEach(item => {
|
|
||||||
if (item.nodeName.toLowerCase() == "publication-information"){
|
|
||||||
item.childNodes.forEach(information => {
|
|
||||||
if (information.nodeName.toLowerCase() == "connector-name") {
|
|
||||||
if (connectorMatch.includes(information.textContent)){
|
|
||||||
connectorNameMatch = 1;
|
|
||||||
} else {
|
|
||||||
connectorNameMatch = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
connectorNameMatch = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
//If the array statusMatch isn't empty then check that the status matches one of the ones
|
|
||||||
//in the array
|
|
||||||
if (statusMatch.length > 0) {
|
|
||||||
publication.childNodes.forEach(item => {
|
|
||||||
if (item.nodeName.toLowerCase() == "publication-status"){
|
|
||||||
if (statusMatch.includes(item.getAttribute('release-status'))) {
|
|
||||||
statusNameMatch = 1;
|
|
||||||
} else {
|
|
||||||
statusNameMatch = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
statusNameMatch = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
//If all of the filtering conditions are met then show the manga, otherwise hide it.
|
|
||||||
if (searchMatch && connectorNameMatch && statusNameMatch) {
|
|
||||||
publication.style.display = 'initial';
|
|
||||||
} else {
|
|
||||||
publication.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsCog.addEventListener("click", () => {
|
|
||||||
OpenSettings();
|
|
||||||
settingsPopup.style.display = "flex";
|
|
||||||
});
|
|
||||||
|
|
||||||
filterFunnel.addEventListener("click", () => {
|
|
||||||
filterBox.classList.toggle("animate");
|
|
||||||
});
|
|
||||||
|
|
||||||
settingKomgaUrl.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingKomgaUser.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingKomgaPass.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingKavitaUrl.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingKavitaUser.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingKavitaPass.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingGotifyUrl.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingGotifyAppToken.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingLunaseaWebhook.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingNtfyEndpoint.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingNtfyAuth.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingUserAgent.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
settingApiUri.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
|
|
||||||
defaultRL.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings();});
|
|
||||||
coverRL.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings();});
|
|
||||||
imageRL.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings();});
|
|
||||||
infoRL.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings();});
|
|
||||||
mDexAuthorRL.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings();});
|
|
||||||
mDexFeedRL.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings();});
|
|
||||||
mDexImageRL.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings();});
|
|
||||||
|
|
||||||
|
|
||||||
function OpenSettings(){
|
|
||||||
settingGotifyConfigured.setAttribute("configuration", "Not Configured");
|
|
||||||
settingLunaseaConfigured.setAttribute("configuration", "Not Configured");
|
|
||||||
settingNtfyConfigured.setAttribute("configuration", "Not Configured");
|
|
||||||
settingKavitaConfigured.setAttribute("configuration", "Not Configured");
|
|
||||||
settingKomgaConfigured.setAttribute("configuration", "Not Configured");
|
|
||||||
settingKomgaUrl.value = "";
|
|
||||||
settingKomgaUser.value = "";
|
|
||||||
settingKomgaPass.value = "";
|
|
||||||
settingKavitaUrl.value = "";
|
|
||||||
settingKavitaUser.value = "";
|
|
||||||
settingKavitaPass.value = "";
|
|
||||||
settingGotifyUrl.value = "";
|
|
||||||
settingGotifyAppToken.value = "";
|
|
||||||
settingLunaseaWebhook.value = "";
|
|
||||||
settingNtfyAuth.value = "";
|
|
||||||
settingNtfyEndpoint.value = "";
|
|
||||||
settingUserAgent.value = "";
|
|
||||||
settingApiUri.value = "";
|
|
||||||
defaultRL.value = "";
|
|
||||||
coverRL.value = "";
|
|
||||||
imageRL.value = "";
|
|
||||||
infoRL.value = "";
|
|
||||||
mDexAuthorRL.value = "";
|
|
||||||
mDexFeedRL.value = "";
|
|
||||||
mDexImageRL.value = "";
|
|
||||||
|
|
||||||
GetSettings().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
settingApiUri.value = apiUri;
|
|
||||||
settingUserAgent.value = json.userAgent;
|
|
||||||
//console.log(json.styleSheet);
|
|
||||||
});
|
|
||||||
GetRateLimits().then((json) => {
|
|
||||||
defaultRL.placeholder = json.Default + ' Requests/Minute';
|
|
||||||
coverRL.placeholder = json.MangaCover + ' Requests/Minute';
|
|
||||||
imageRL.placeholder = json.MangaImage + ' Requests/Minute';
|
|
||||||
infoRL.placeholder = json.MangaInfo + ' Requests/Minute';
|
|
||||||
mDexAuthorRL.placeholder = json.MangaDexAuthor + ' Requests/Minute';
|
|
||||||
mDexFeedRL.placeholder = json.MangaDexFeed + ' Requests/Minute';
|
|
||||||
mDexImageRL.placeholder = json.MangaDexImage + ' Requests/Minute';
|
|
||||||
});
|
|
||||||
GetLibraryConnectors().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
json.forEach(connector => {
|
|
||||||
switch(libraryConnectorTypes[connector.libraryType]){
|
|
||||||
case "Kavita":
|
|
||||||
settingKavitaConfigured.setAttribute("configuration", "Active");
|
|
||||||
settingKavitaUrl.value = connector.baseUrl;
|
|
||||||
settingKavitaUser.value = "***";
|
|
||||||
settingKavitaPass.value = "***";
|
|
||||||
break;
|
|
||||||
case "Komga":
|
|
||||||
settingKomgaConfigured.setAttribute("configuration", "Active");
|
|
||||||
settingKomgaUrl.value = connector.baseUrl;
|
|
||||||
settingKomgaUser.value = "***";
|
|
||||||
settingKomgaPass.value = "***";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Unknown type");
|
|
||||||
console.log(connector);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
GetNotificationConnectors().then((json) => {
|
|
||||||
json.forEach(connector => {
|
|
||||||
switch(notificationConnectorTypes[connector.notificationConnectorType]){
|
|
||||||
case "Gotify":
|
|
||||||
settingGotifyUrl.value = connector.endpoint;
|
|
||||||
settingGotifyAppToken.value = "***";
|
|
||||||
settingGotifyConfigured.setAttribute("configuration", "Active");
|
|
||||||
break;
|
|
||||||
case "LunaSea":
|
|
||||||
settingLunaseaConfigured.setAttribute("configuration", "Active");
|
|
||||||
settingLunaseaWebhook.value = connector.id;
|
|
||||||
break;
|
|
||||||
case "Ntfy":
|
|
||||||
settingNtfyConfigured.setAttribute("configuration", "Active");
|
|
||||||
settingNtfyEndpoint.value = connector.endpoint;
|
|
||||||
settingNtfyAuth.value = "***";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Unknown type");
|
|
||||||
console.log(connector);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//Functions for clearing/resetting connectors in the settings pop-up
|
|
||||||
function ClearKomga(){
|
|
||||||
settingKomgaUrl.value = "";
|
|
||||||
settingKomgaUser.value = "";
|
|
||||||
settingKomgaPass.value = "";
|
|
||||||
settingKomgaConfigured.setAttribute("configuration", "Not Configured");
|
|
||||||
ResetKomga();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClearKavita(){
|
|
||||||
settingKavitaUrl.value = "";
|
|
||||||
settingKavitaUser.value = "";
|
|
||||||
settingKavitaPass.value = "";
|
|
||||||
settingKavitaConfigured.setAttribute("configuration", "Not Configured");
|
|
||||||
ResetKavita();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClearGotify(){
|
|
||||||
settingGotifyUrl.value = "";
|
|
||||||
settingGotifyAppToken.value = ""
|
|
||||||
settingGotifyConfigured.setAttribute("configuration", "Not Configured");
|
|
||||||
ResetGotify();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClearLunasea(){
|
|
||||||
settingLunaseaWebhook.value = "";
|
|
||||||
settingLunaseaConfigured.setAttribute("configuration", "Not Configured");
|
|
||||||
ResetLunaSea();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClearNtfy(){
|
|
||||||
settingNtfyEndpoint.value = "";
|
|
||||||
settingNtfyAuth.value = "";
|
|
||||||
settingNtfyConfigured.setAttribute("configuration", "Not Configured");
|
|
||||||
ResetNtfy();
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateSettings(){
|
|
||||||
if(settingApiUri.value != ""){
|
|
||||||
apiUri = settingApiUri.value;
|
|
||||||
setCookie("apiUri", apiUri);
|
|
||||||
Setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(settingKomgaUrl.value != "" &&
|
|
||||||
settingKomgaUser.value != "" &&
|
|
||||||
settingKomgaPass.value != ""){
|
|
||||||
UpdateKomga(settingKomgaUrl.value, utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(settingKavitaUrl.value != "" &&
|
|
||||||
settingKavitaUser.value != "" &&
|
|
||||||
settingKavitaPass.value != ""){
|
|
||||||
UpdateKavita(settingKavitaUrl.value, settingKavitaUser.value, settingKavitaPass.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(settingGotifyUrl.value != "" &&
|
|
||||||
settingGotifyAppToken.value != ""){
|
|
||||||
UpdateGotify(settingGotifyUrl.value, settingGotifyAppToken.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(settingLunaseaWebhook.value != ""){
|
|
||||||
UpdateLunaSea(settingLunaseaWebhook.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(settingNtfyEndpoint.value != "" &&
|
|
||||||
settingNtfyAuth.value != ""){
|
|
||||||
UpdateNtfy(settingNtfyEndpoint.value, settingNtfyAuth.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(settingUserAgent.value != ""){
|
|
||||||
UpdateUserAgent(settingUserAgent.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultRL.value != "") {
|
|
||||||
UpdateRateLimit(0, defaultRL.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coverRL.value != "") {
|
|
||||||
UpdateRateLimit(3, coverRL.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageRL.value != "") {
|
|
||||||
UpdateRateLimit(2, imageRL.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (infoRL.value != "") {
|
|
||||||
UpdateRateLimit(6, infoRL.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mDexAuthorRL.value != "") {
|
|
||||||
UpdateRateLimit(5, mDexAuthorRL.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mDexFeedRL.value != "") {
|
|
||||||
UpdateRateLimit(1, mDexFeedRL.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mDexImageRL.value != "") {
|
|
||||||
UpdateRateLimit(5, mDexImageRL.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
OpenSettings();
|
|
||||||
Setup();
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
function utf8_to_b64(str) {
|
|
||||||
return window.btoa(unescape(encodeURIComponent( str )));
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateJobs(){
|
|
||||||
|
|
||||||
GetMonitorJobs().then((json) => {
|
|
||||||
if(monitoringJobsCount != json.length){
|
|
||||||
ResetContent();
|
|
||||||
monitoringJobsCount = json.length;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Get the jobs that are waiting in the queue
|
|
||||||
GetWaitingJobs().then((json) => {
|
|
||||||
jobsQueuedTag.innerText = json.length;
|
|
||||||
|
|
||||||
var nowWaitingJobs = [];
|
|
||||||
|
|
||||||
json.forEach(job => {
|
|
||||||
if(!waitingJobs.includes(GetValidSelector(job.id))){
|
|
||||||
var jobDom = createJob(job);
|
|
||||||
jobStatusWaiting.appendChild(jobDom);
|
|
||||||
}
|
|
||||||
nowWaitingJobs.push(GetValidSelector(job.id));
|
|
||||||
});
|
|
||||||
waitingJobs = nowWaitingJobs;
|
|
||||||
});
|
|
||||||
|
|
||||||
jobStatusWaiting.childNodes.forEach(child => {
|
|
||||||
if(!waitingJobs.includes(child.id))
|
|
||||||
jobStatusWaiting.removeChild(child);
|
|
||||||
});
|
|
||||||
|
|
||||||
//Get currently running jobs
|
|
||||||
GetRunningJobs().then((json) => {
|
|
||||||
jobsRunningTag.innerText = json.length;
|
|
||||||
|
|
||||||
var nowRunningJobs = [];
|
|
||||||
|
|
||||||
json.forEach(job => {
|
|
||||||
if(!runningJobs.includes(GetValidSelector(job.id))){
|
|
||||||
var jobDom = createJob(job);
|
|
||||||
jobStatusRunning.appendChild(jobDom);
|
|
||||||
}
|
|
||||||
nowRunningJobs.push(GetValidSelector(job.id));
|
|
||||||
UpdateJobProgress(job.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
runningJobs = nowRunningJobs;
|
|
||||||
});
|
|
||||||
|
|
||||||
jobStatusRunning.childNodes.forEach(child => {
|
|
||||||
if(!runningJobs.includes(child.id))
|
|
||||||
jobStatusRunning.removeChild(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createJob(jobjson){
|
|
||||||
var manga;
|
|
||||||
if(jobjson.chapter != null)
|
|
||||||
manga = jobjson.chapter.parentManga;
|
|
||||||
else if(jobjson.manga != null)
|
|
||||||
manga = jobjson.manga;
|
|
||||||
else return null;
|
|
||||||
|
|
||||||
|
|
||||||
var wrapper = document.createElement("div");
|
|
||||||
wrapper.className = "section-item";
|
|
||||||
wrapper.id = GetValidSelector(jobjson.id);
|
|
||||||
|
|
||||||
var image = document.createElement("img");
|
|
||||||
image.className = "jobImage";
|
|
||||||
image.src = GetCoverUrl(manga.internalId);
|
|
||||||
wrapper.appendChild(image);
|
|
||||||
|
|
||||||
var details = document.createElement("div");
|
|
||||||
details.className = 'jobDetails';
|
|
||||||
|
|
||||||
var title = document.createElement("span");
|
|
||||||
title.className = "jobTitle";
|
|
||||||
if(jobjson.chapter != null)
|
|
||||||
title.innerText = `${manga.sortName} - ${jobjson.chapter.fileName}`;
|
|
||||||
else if(jobjson.manga != null)
|
|
||||||
title.innerText = manga.sortName;
|
|
||||||
details.appendChild(title);
|
|
||||||
|
|
||||||
var progressBar = document.createElement("progress");
|
|
||||||
progressBar.className = "jobProgressBar";
|
|
||||||
progressBar.id = `jobProgressBar${GetValidSelector(jobjson.id)}`;
|
|
||||||
details.appendChild(progressBar);
|
|
||||||
|
|
||||||
var progressSpan = document.createElement("span");
|
|
||||||
progressSpan.className = "jobProgressSpan";
|
|
||||||
progressSpan.id = `jobProgressSpan${GetValidSelector(jobjson.id)}`;
|
|
||||||
progressSpan.innerText = "Pending...";
|
|
||||||
details.appendChild(progressSpan);
|
|
||||||
|
|
||||||
var cancelSpan = document.createElement("span");
|
|
||||||
cancelSpan.className = "jobCancel";
|
|
||||||
cancelSpan.innerText = "Cancel";
|
|
||||||
cancelSpan.addEventListener("click", () => CancelJob(jobjson.id));
|
|
||||||
details.appendChild(cancelSpan);
|
|
||||||
|
|
||||||
wrapper.appendChild(details);
|
|
||||||
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShowJobQueue(){
|
|
||||||
jobStatusView.style.display = "initial";
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateJobProgress(jobId){
|
|
||||||
GetProgress(jobId).then((json) => {
|
|
||||||
var progressBar = document.querySelector(`#jobProgressBar${GetValidSelector(jobId)}`);
|
|
||||||
var progressSpan = document.querySelector(`#jobProgressSpan${GetValidSelector(jobId)}`);
|
|
||||||
if(progressBar != null && json.progress != 0){
|
|
||||||
progressBar.value = json.progress;
|
|
||||||
}
|
|
||||||
if(progressSpan != null){
|
|
||||||
var percentageStr = "0%";
|
|
||||||
var timeleftStr = "00:00:00";
|
|
||||||
if(json.progress != 0){
|
|
||||||
percentageStr = Intl.NumberFormat("en-US", { style: "percent"}).format(json.progress);
|
|
||||||
timeleftStr = json.timeRemaining.split('.')[0];
|
|
||||||
}
|
|
||||||
progressSpan.innerText = `${percentageStr} ${timeleftStr}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function GetValidSelector(str){
|
|
||||||
var clean = [...str.matchAll(/[a-zA-Z0-9]*-*_*/g)];
|
|
||||||
return clean.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
const stringToColour = (str) => {
|
|
||||||
let hash = 0;
|
|
||||||
str.split('').forEach(char => {
|
|
||||||
hash = char.charCodeAt(0) + ((hash << 5) - hash)
|
|
||||||
})
|
|
||||||
let colour = '#'
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const value = (hash >> (i * 8)) & 0xff
|
|
||||||
colour += value.toString(16).padStart(2, '0')
|
|
||||||
}
|
|
||||||
return colour
|
|
||||||
}
|
|
BIN
Website/media/connector-icons/bato.ico
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Website/media/connector-icons/gotify-logo.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
Website/media/connector-icons/kavita.png
Normal file
After Width: | Height: | Size: 440 B |
43
Website/media/connector-icons/komga.svg
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?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>
|
After Width: | Height: | Size: 4.2 KiB |
BIN
Website/media/connector-icons/lunasea.png
Normal file
After Width: | Height: | Size: 64 KiB |
1
Website/media/connector-icons/mangadex-logo.svg
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
Website/media/connector-icons/mangakatana.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
Website/media/connector-icons/mangalife.png
Normal file
After Width: | Height: | Size: 477 B |
BIN
Website/media/connector-icons/manganato.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
Website/media/connector-icons/mangasee.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
Website/media/connector-icons/mangaworld.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
40
Website/media/connector-icons/ntfy.svg
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 4.6 KiB |
7
Website/media/link.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?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>
|
After Width: | Height: | Size: 304 B |
@ -1,7 +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 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
|
||||||
<g fill="#000000">
|
|
||||||
<path d="M2.23 2.674a.75.75 0 00-.96 1.152L3.578 5.75 1.27 7.674a.75.75 0 00.96 1.152l3-2.5a.75.75 0 000-1.152l-3-2.5zM8.25 5a.75.75 0 000 1.5h6a.75.75 0 000-1.5h-6zM5.5 9.25a.75.75 0 01.75-.75h8a.75.75 0 010 1.5h-8a.75.75 0 01-.75-.75zM6.25 12a.75.75 0 000 1.5h8a.75.75 0 000-1.5h-8z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 545 B |
@ -1,53 +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 235.504 235.504"
|
|
||||||
xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path d="M195.209,81.456l-49.227-0.15c0.737-0.886,1.351-1.868,2.284-2.583c3.282-2.497,3.911-7.166,1.427-10.438
|
|
||||||
c-2.501-3.266-7.161-3.919-10.443-1.423c-4.873,3.715-8.388,8.704-10.255,14.389l-22.191-0.064
|
|
||||||
c-9.508,0-19.588,7.398-22.938,16.851l-16.877,47.479c-1.775,5.013-1.338,9.966,1.207,13.568
|
|
||||||
c2.412,3.427,6.384,5.318,11.187,5.358l45.126,0.136c-1.509,5.186-4.701,9.622-9.352,12.424
|
|
||||||
c-4.891,2.957-10.636,3.814-16.172,2.444c-3.994-0.998-8.031,1.442-9.027,5.418c-0.99,4.012,1.445,8.035,5.432,9.032
|
|
||||||
c2.927,0.738,5.879,1.091,8.808,1.091c6.516,0,12.93-1.788,18.645-5.23c8.312-5.013,14.172-12.979,16.484-22.409
|
|
||||||
c0.232-0.905,0.232-1.823,0.124-2.713l28.296,0.092h0.049c2.925,0,5.854-0.89,8.684-2.147c0.2,0.493,0.32,1.014,0.661,1.471
|
|
||||||
c3.335,4.677,4.629,10.343,3.688,15.993c-0.95,5.627-4.028,10.536-8.688,13.862c-3.351,2.376-4.14,7.037-1.755,10.379
|
|
||||||
c1.466,2.04,3.751,3.122,6.062,3.122c1.491,0,3.006-0.429,4.312-1.367c7.919-5.61,13.16-13.966,14.771-23.52
|
|
||||||
c1.603-9.565-0.613-19.203-6.28-27.122c-0.48-0.693-1.134-1.19-1.779-1.659c1.318-1.831,2.501-3.763,3.238-5.854l16.863-47.464
|
|
||||||
c1.795-5.018,1.351-9.969-1.194-13.58C203.954,83.387,200.015,81.47,195.209,81.456z M201.979,98.405l-16.868,47.464
|
|
||||||
c-0.981,2.757-2.941,5.214-5.213,7.329c-0.337,0.16-0.706,0.229-1.026,0.465c-0.673,0.485-1.182,1.122-1.639,1.747
|
|
||||||
c-2.962,1.996-6.288,3.339-9.434,3.339v2.989l-0.044-2.989l-33.194-0.101c-0.232-0.076-0.424-0.261-0.661-0.324
|
|
||||||
c-1.435-0.353-2.805-0.145-4.095,0.309l-29.768-0.101l1.192-3.358c0.549-1.547-0.269-3.25-1.813-3.795
|
|
||||||
c-1.521-0.553-3.25,0.24-3.799,1.804l-1.899,5.334l-14.318-0.044c-2.805,0-5.063-0.998-6.336-2.813
|
|
||||||
c-1.437-2.032-1.603-4.921-0.463-8.144l16.877-47.478c2.48-6.979,10.417-12.868,17.356-12.868l12.217,0.038l-1.963,5.536
|
|
||||||
c-0.555,1.549,0.262,3.25,1.805,3.797c0.331,0.12,0.661,0.174,0.998,0.174c1.227,0,2.372-0.768,2.793-1.986l2.497-7.019
|
|
||||||
c0.064-0.164-0.048-0.322-0.016-0.487h2.512c-0.905,7.758,1.163,15.42,5.947,21.638c5.903,7.687,14.852,11.726,23.873,11.726
|
|
||||||
c6.371,0,12.771-2.001,18.186-6.129c3.266-2.488,3.911-7.167,1.426-10.441c-2.508-3.267-7.161-3.901-10.455-1.415
|
|
||||||
c-6.612,5.056-16.146,3.775-21.223-2.809c-2.445-3.194-3.487-7.133-2.958-11.117c0.061-0.503,0.353-0.916,0.481-1.402
|
|
||||||
l52.216,0.156c2.806,0,5.054,1.004,6.324,2.811C202.928,92.241,203.105,95.223,201.979,98.405z"/>
|
|
||||||
<path d="M107.997,127.194c-1.531-0.553-3.248,0.244-3.799,1.791l-4.302,12.099c-0.551,1.543,0.265,3.242,1.813,3.795
|
|
||||||
c0.331,0.116,0.659,0.16,0.998,0.16c1.214,0,2.372-0.765,2.801-1.976l4.294-12.099
|
|
||||||
C110.369,129.446,109.551,127.728,107.997,127.194z"/>
|
|
||||||
<path d="M116.6,103.014c-1.529-0.541-3.25,0.252-3.805,1.805l-4.298,12.088c-0.547,1.547,0.261,3.252,1.799,3.799
|
|
||||||
c0.329,0.12,0.659,0.172,1,0.172c1.222,0,2.368-0.769,2.809-1.983l4.294-12.09C118.955,105.268,118.139,103.555,116.6,103.014z"/>
|
|
||||||
<path d="M232.527,90.428l-14.896-0.038l0,0c-1.639,0-2.974,1.327-2.997,2.976c0,1.639,1.342,2.981,2.981,2.989l14.896,0.042l0,0
|
|
||||||
c1.643,0,2.978-1.331,2.993-2.979C235.504,91.763,234.17,90.436,232.527,90.428z"/>
|
|
||||||
<path d="M220.333,80.436c0.629,0,1.242-0.188,1.771-0.583l11.994-8.83c1.326-0.974,1.611-2.842,0.645-4.168
|
|
||||||
c-0.965-1.327-2.845-1.611-4.163-0.637l-11.998,8.833c-1.323,0.974-1.607,2.841-0.642,4.167
|
|
||||||
C218.513,80.003,219.418,80.436,220.333,80.436z"/>
|
|
||||||
<path d="M209.152,56.279c-1.547-0.549-3.25,0.269-3.787,1.805l-4.997,14.036c-0.537,1.547,0.26,3.252,1.803,3.807
|
|
||||||
c0.337,0.12,0.674,0.172,0.994,0.172c1.242,0,2.385-0.757,2.821-1.986l4.985-14.036C211.516,58.541,210.695,56.846,209.152,56.279
|
|
||||||
z"/>
|
|
||||||
<path d="M17.587,100.894h55.208c1.641,0,2.976-1.343,2.976-2.981c0-1.641-1.334-2.988-2.976-2.988H17.587
|
|
||||||
c-1.641,0-2.988,1.338-2.988,2.988C14.599,99.559,15.946,100.894,17.587,100.894z"/>
|
|
||||||
<path d="M68.471,119.328c0-1.641-1.345-2.987-2.986-2.987H10.283c-1.639,0-2.981,1.338-2.981,2.987
|
|
||||||
c0,1.639,1.342,2.974,2.981,2.974h55.202C67.119,122.301,68.471,120.967,68.471,119.328z"/>
|
|
||||||
<path d="M58.188,137.758H2.974c-1.641,0-2.974,1.335-2.974,2.989c0,1.64,1.333,2.974,2.974,2.974h55.214
|
|
||||||
c1.639,0,2.981-1.334,2.981-2.974C61.162,139.093,59.827,137.758,58.188,137.758z"/>
|
|
||||||
<path d="M169.611,28.097c11.821,0,21.403,9.584,21.403,21.41c0,11.82-9.582,21.408-21.403,21.408
|
|
||||||
c-11.822,0-21.412-9.588-21.412-21.408C148.199,37.681,157.789,28.097,169.611,28.097z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 4.6 KiB |
@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg fill="#000000" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
|
||||||
<g id="task">
|
|
||||||
<path d="M4,23.4l-3.7-3.7l1.4-1.4L4,20.6l4.3-4.3l1.4,1.4L4,23.4z M24,21H12v-2h12V21z M4,15.4l-3.7-3.7l1.4-1.4L4,12.6l4.3-4.3
|
|
||||||
l1.4,1.4L4,15.4z M24,13H12v-2h12V13z M4,7.4L0.3,3.7l1.4-1.4L4,4.6l4.3-4.3l1.4,1.4L4,7.4z M24,5H12V3h12V5z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 603 B |
48
Website/modules/Footer.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React, {useEffect} from 'react';
|
||||||
|
import '../styles/footer.css';
|
||||||
|
import Job from './Job';
|
||||||
|
import Icon from '@mdi/react';
|
||||||
|
import { mdiRun, mdiCounter, mdiEyeCheck, mdiTrayFull } from '@mdi/js';
|
||||||
|
import QueuePopUp from "./QueuePopUp";
|
||||||
|
|
||||||
|
export default function Footer({connectedToBackend, apiUri} : {connectedToBackend: boolean, apiUri: string}) {
|
||||||
|
const [MonitoringJobsCount, setMonitoringJobsCount] = React.useState(0);
|
||||||
|
const [AllJobsCount, setAllJobsCount] = React.useState(0);
|
||||||
|
const [RunningJobsCount, setRunningJobsCount] = React.useState(0);
|
||||||
|
const [StandbyJobsCount, setStandbyJobsCount] = React.useState(0);
|
||||||
|
const [countUpdateInterval, setCountUpdateInterval] = React.useState<number>();
|
||||||
|
|
||||||
|
function UpdateBackendState(){
|
||||||
|
Job.GetMonitoringJobs(apiUri).then((jobs) => setMonitoringJobsCount(jobs.length));
|
||||||
|
Job.GetAllJobs(apiUri).then((jobs) => setAllJobsCount(jobs.length));
|
||||||
|
Job.GetRunningJobs(apiUri).then((jobs) => setRunningJobsCount(jobs.length));
|
||||||
|
Job.GetStandbyJobs(apiUri).then((jobs) => setStandbyJobsCount(jobs.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(connectedToBackend){
|
||||||
|
UpdateBackendState();
|
||||||
|
setCountUpdateInterval(setInterval(() => {
|
||||||
|
UpdateBackendState();
|
||||||
|
}, 2000));
|
||||||
|
}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}>
|
||||||
|
<div className="statusBadge hoverHand"><Icon path={mdiRun} size={1}/> <span>{RunningJobsCount}</span>
|
||||||
|
</div>
|
||||||
|
<span>+</span>
|
||||||
|
<div className="statusBadge hoverHand"><Icon path={mdiTrayFull} size={1}/><span>{StandbyJobsCount}</span></div>
|
||||||
|
</QueuePopUp>
|
||||||
|
<span>=</span>
|
||||||
|
<div className="statusBadge"><Icon path={mdiCounter} size={1}/> <span>{AllJobsCount}</span></div>
|
||||||
|
<p id="madeWith">Made with Blåhaj 🦈</p>
|
||||||
|
</footer>)
|
||||||
|
}
|
15
Website/modules/Header.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../styles/header.css'
|
||||||
|
import Settings from "./Settings";
|
||||||
|
import IFrontendSettings from "./interfaces/IFrontendSettings";
|
||||||
|
|
||||||
|
export default function Header({backendConnected, apiUri, settings, changeSettings} : {backendConnected: boolean, apiUri: string, settings: IFrontendSettings, changeSettings(settings: IFrontendSettings): void}){
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
<div id="titlebox">
|
||||||
|
<img alt="website image is Blahaj" src="../media/blahaj.png"/>
|
||||||
|
<span>Tranga</span>
|
||||||
|
</div>
|
||||||
|
<Settings settings={settings} changeSettings={changeSettings} backendConnected={backendConnected} apiUri={apiUri}/>
|
||||||
|
</header>)
|
||||||
|
}
|
142
Website/modules/Job.tsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import {deleteData, getData, postData} from '../App';
|
||||||
|
import IJob from "./interfaces/IJob";
|
||||||
|
import IProgressToken from "./interfaces/IProgressToken";
|
||||||
|
|
||||||
|
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<string[]> {
|
||||||
|
//console.info("Getting all Jobs");
|
||||||
|
return getData(`${apiUri}/v2/Jobs`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info("Got all Jobs");
|
||||||
|
const ret = json as string[];
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async GetRunningJobs(apiUri: string): Promise<string[]> {
|
||||||
|
//console.info("Getting all running Jobs");
|
||||||
|
return getData(`${apiUri}/v2/Jobs/Running`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info("Got all running Jobs");
|
||||||
|
const ret = json as string[];
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async GetWaitingJobs(apiUri: string): Promise<string[]> {
|
||||||
|
//console.info("Getting all waiting Jobs");
|
||||||
|
return getData(`${apiUri}/v2/Jobs/Waiting`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info("Got all waiting Jobs");
|
||||||
|
const ret = json as string[];
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async GetStandbyJobs(apiUri: string): Promise<string[]> {
|
||||||
|
//console.info("Getting all standby Jobs");
|
||||||
|
return getData(`${apiUri}/v2/Jobs/Standby`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info("Got all standby Jobs");
|
||||||
|
const ret = json as string[];
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async GetMonitoringJobs(apiUri: string): Promise<string[]> {
|
||||||
|
//console.info("Getting all monitoring Jobs");
|
||||||
|
return getData(`${apiUri}/v2/Jobs/Monitoring`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info("Got all monitoring Jobs");
|
||||||
|
const ret = json as string[];
|
||||||
|
//console.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 async GetJobs(apiUri: string, jobIds: string[]): Promise<IJob[]> {
|
||||||
|
if(jobIds === undefined || jobIds === null || jobIds.length < 1) {
|
||||||
|
console.error(`JobIds was not provided`);
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
let reqStr = jobIds.join(",");
|
||||||
|
//console.info(`Getting Jobs ${reqStr}`);
|
||||||
|
return getData(`${apiUri}/v2/Job?jobIds=${reqStr}`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Got Jobs ${reqStr}`);
|
||||||
|
const ret = json as IJob[];
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async GetJobProgress(apiUri: string, jobId: string): Promise<IProgressToken> {
|
||||||
|
//console.info(`Getting Job ${jobId} Progress`);
|
||||||
|
return getData(`${apiUri}/v2/Job/${jobId}/Progress`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Got Job ${jobId} Progress`);
|
||||||
|
const ret = json as IProgressToken;
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async CreateJobDateInterval(apiUri: string, internalId: string, jobType: string, interval: Date) : Promise<null> {
|
||||||
|
return this.CreateJob(apiUri, internalId, jobType, this.IntervalStringFromDate(interval));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async CreateJob(apiUri: string, internalId: string, jobType: string, interval: string): Promise<null> {
|
||||||
|
const validate = /(?:[0-9]{1,2}\.)?[0-9]{1,2}:[0-9]{1,2}(?::[0-9]{1,2})?/
|
||||||
|
//console.info(`Creating Job for Manga ${internalId} at ${interval} interval`);
|
||||||
|
if(!validate.test(interval)){
|
||||||
|
console.error("Interval was in incorrect format.");
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
internalId: internalId,
|
||||||
|
interval: interval
|
||||||
|
};
|
||||||
|
return postData(`${apiUri}/v2/Job/Create/${jobType}`, data)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Created Job for Manga ${internalId} at ${interval} interval`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static DeleteJob(apiUri: string, jobId: string) : Promise<void> {
|
||||||
|
return deleteData(`${apiUri}/v2/Job/${jobId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static StartJob(apiUri: string, jobId: string) : Promise<object> {
|
||||||
|
return postData(`${apiUri}/v2/Job/${jobId}/StartNow`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
static CancelJob(apiUri: string, jobId: string) : Promise<object> {
|
||||||
|
return postData(`${apiUri}/v2/Job/${jobId}/Cancel`, {});
|
||||||
|
}
|
||||||
|
}
|
135
Website/modules/LibraryConnector.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import {deleteData, getData, postData} from "../App";
|
||||||
|
import ILibraryConnector from "./interfaces/ILibraryConnector";
|
||||||
|
|
||||||
|
export default abstract class LibraryConnector
|
||||||
|
{
|
||||||
|
static async GetLibraryConnectors(apiUri: string) : Promise<ILibraryConnector[]> {
|
||||||
|
//console.info("Getting Library Connectors");
|
||||||
|
return getData(`${apiUri}/v2/LibraryConnector`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info("Got Library Connectors");
|
||||||
|
const ret = json as ILibraryConnector[];
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
})
|
||||||
|
.catch(Promise.reject);
|
||||||
|
}
|
||||||
|
public url = "";
|
||||||
|
|
||||||
|
protected constructor(url: string) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SetUrl(url: string){
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Test(apiUri: string) : Promise<boolean>;
|
||||||
|
public abstract Reset(apiUri: string) : Promise<boolean>;
|
||||||
|
public abstract Create(apiUri: string) : Promise<boolean>;
|
||||||
|
protected abstract CheckConnector() : boolean;
|
||||||
|
|
||||||
|
protected async TestConnector(apiUri: string, connectorType: string, data: object): Promise<boolean> {
|
||||||
|
if(!this.CheckConnector())
|
||||||
|
return Promise.reject("Connector not fully configured.");
|
||||||
|
//console.info(`Testing ${connectorType}`);
|
||||||
|
return postData(`${apiUri}/v2/LibraryConnector/${connectorType}/Test`, data)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Successfully tested ${connectorType}`);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(Promise.reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async ResetConnector(apiUri: string, connectorType: string): Promise<boolean> {
|
||||||
|
//console.info(`Deleting ${connectorType}`);
|
||||||
|
return deleteData(`${apiUri}/v2/LibraryConnector/${connectorType}`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Successfully deleted ${connectorType}`);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(Promise.reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async CreateConnector(apiUri: string, connectorType: string, data: object): Promise<boolean> {
|
||||||
|
if(!this.CheckConnector())
|
||||||
|
return Promise.reject("Connector not fully configured.");
|
||||||
|
//console.info(`Creating ${connectorType}`);
|
||||||
|
return postData(`${apiUri}/v2/LibraryConnector/${connectorType}`, data)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Successfully created ${connectorType}`);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(Promise.reject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Komga extends LibraryConnector
|
||||||
|
{
|
||||||
|
private username = "";
|
||||||
|
private password = "";
|
||||||
|
|
||||||
|
constructor({url, username, password} : {url: string, username: string, password: string}){
|
||||||
|
super(url);
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Test(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.TestConnector(apiUri, "Komga", {url: this.url, username: this.username, password: this.password}).then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Reset(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.ResetConnector(apiUri, "Komga").then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Create(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.CreateConnector(apiUri, "Komga", {url: this.url, username: this.username, password: this.password}).then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CheckConnector(): boolean {
|
||||||
|
try{
|
||||||
|
new URL(this.url)
|
||||||
|
}catch{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(this.username.length < 1 || this.password.length < 1)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Kavita extends LibraryConnector
|
||||||
|
{
|
||||||
|
private username = "";
|
||||||
|
private password = "";
|
||||||
|
|
||||||
|
constructor({url, username, password} : {url: string, username: string, password: string}) {
|
||||||
|
super(url);
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Test(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.TestConnector(apiUri, "Kavita", {url: this.url, username: this.username, password: this.password}).then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Reset(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.ResetConnector(apiUri, "Kavita").then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Create(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.CreateConnector(apiUri, "Kavita", {url: this.url, username: this.username, password: this.password}).then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CheckConnector(): boolean {
|
||||||
|
try{
|
||||||
|
new URL(this.url)
|
||||||
|
}catch{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(this.username.length < 1 || this.password.length < 1)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
54
Website/modules/Manga.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import IManga from './interfaces/IManga';
|
||||||
|
import { getData } from '../App';
|
||||||
|
|
||||||
|
export default class Manga
|
||||||
|
{
|
||||||
|
static async GetAllManga(apiUri: string): Promise<IManga[]> {
|
||||||
|
//console.info("Getting all Manga");
|
||||||
|
return getData(`${apiUri}/v2/Mangas`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info("Got all Manga");
|
||||||
|
const ret = json as IManga[];
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async SearchManga(apiUri: string, name: string): Promise<IManga[]> {
|
||||||
|
//console.info(`Getting Manga ${name} from all Connectors`);
|
||||||
|
return await getData(`${apiUri}/v2/Manga/Search?title=${name}`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Got Manga ${name}`);
|
||||||
|
const ret = json as IManga[];
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async GetMangaById(apiUri: string, internalId: string): Promise<IManga> {
|
||||||
|
//console.info(`Getting Manga ${internalId}`);
|
||||||
|
return await getData(`${apiUri}/v2/Manga/${internalId}`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Got Manga ${internalId}`);
|
||||||
|
const ret = json as IManga;
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async GetMangaByIds(apiUri: string, internalIds: string[]): Promise<IManga[]> {
|
||||||
|
//console.debug(`Getting Mangas ${internalIds.join(",")}`);
|
||||||
|
return await getData(`${apiUri}/v2/Manga?mangaIds=${internalIds.join(",")}`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.debug(`Got Manga ${internalIds.join(",")}`);
|
||||||
|
const ret = json as IManga[];
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static GetMangaCoverUrl(apiUri: string, internalId: string, ref: HTMLElement): string {
|
||||||
|
//console.debug(`Getting Manga Cover-Url ${internalId}`);
|
||||||
|
return `${apiUri}/v2/Manga/${internalId}/Cover?dimensions=${ref.clientWidth*1.5}x${ref.clientHeight*1.5}`;
|
||||||
|
}
|
||||||
|
}
|
33
Website/modules/MangaConnector.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import IMangaConnector from './interfaces/IMangaConnector';
|
||||||
|
import IManga from './interfaces/IManga';
|
||||||
|
import { getData } from '../App';
|
||||||
|
|
||||||
|
export class MangaConnector
|
||||||
|
{
|
||||||
|
static async GetAllConnectors(): Promise<IMangaConnector[]> {
|
||||||
|
//console.info("Getting all MangaConnectors");
|
||||||
|
return getData("http://127.0.0.1:6531/v2/Connector/Types")
|
||||||
|
.then((json) => {
|
||||||
|
//console.info("Got all MangaConnectors");
|
||||||
|
return (json as IMangaConnector[]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async GetMangaFromConnectorByTitle(connector: IMangaConnector, name: string): Promise<IManga[]> {
|
||||||
|
//console.info(`Getting Manga ${name}`);
|
||||||
|
return await getData(`http://127.0.0.1:6531/v2/Connector/${connector.name}/GetManga?title=${name}`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Got Manga ${name}`);
|
||||||
|
return (json as IManga[]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async GetMangaFromConnectorByUrl(connector: IMangaConnector, url: string): Promise<IManga> {
|
||||||
|
//console.info(`Getting Manga ${url}`);
|
||||||
|
return await getData(`http://127.0.0.1:6531/v2/Connector/${connector.name}/GetManga?url=${url}`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Got Manga ${url}`);
|
||||||
|
return (json as IManga);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
100
Website/modules/MonitorJobsList.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, {EventHandler, MouseEventHandler, ReactElement, useEffect, useState} from 'react';
|
||||||
|
import Job from './Job';
|
||||||
|
import '../styles/monitorMangaList.css';
|
||||||
|
import IJob from "./interfaces/IJob";
|
||||||
|
import IManga, {CoverCard} from "./interfaces/IManga";
|
||||||
|
import Manga from './Manga';
|
||||||
|
import '../styles/MangaCoverCard.css'
|
||||||
|
import Icon from '@mdi/react';
|
||||||
|
import { mdiTrashCanOutline, mdiPlayBoxOutline } from '@mdi/js';
|
||||||
|
|
||||||
|
export default function MonitorJobsList({onStartSearch, onJobsChanged, connectedToBackend, apiUri, updateList} : {onStartSearch() : void, onJobsChanged: EventHandler<any>, connectedToBackend: boolean, apiUri: string, updateList: Date}) {
|
||||||
|
const [MonitoringJobs, setMonitoringJobs] = useState<IJob[]>([]);
|
||||||
|
const [AllManga, setAllManga] = useState<IManga[]>([]);
|
||||||
|
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//console.debug("Updating display list.");
|
||||||
|
//Remove all Manga that are not associated with a Job
|
||||||
|
setAllManga(AllManga.filter(manga => MonitoringJobs.find(job => job.mangaInternalId == manga.internalId)));
|
||||||
|
//Fetch Manga that are missing (from Jobs)
|
||||||
|
if(MonitoringJobs === null)
|
||||||
|
return;
|
||||||
|
MonitoringJobs.forEach(job => {
|
||||||
|
if(AllManga.find(manga => manga.internalId == job.mangaInternalId))
|
||||||
|
return;
|
||||||
|
Manga.GetMangaById(apiUri, job.mangaInternalId != undefined ? job.mangaInternalId : job.chapter != undefined ? job.chapter.parentManga.internalId : "").
|
||||||
|
then((manga: IManga) => setAllManga([...AllManga, manga]));
|
||||||
|
});
|
||||||
|
}, [MonitoringJobs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(connectedToBackend){
|
||||||
|
UpdateMonitoringJobsList();
|
||||||
|
setJoblistUpdateInterval(setInterval(() => {
|
||||||
|
UpdateMonitoringJobsList();
|
||||||
|
}, 1000));
|
||||||
|
}else{
|
||||||
|
clearInterval(joblistUpdateInterval);
|
||||||
|
setJoblistUpdateInterval(undefined);
|
||||||
|
}
|
||||||
|
}, [connectedToBackend]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
UpdateMonitoringJobsList();
|
||||||
|
}, [updateList]);
|
||||||
|
|
||||||
|
function UpdateMonitoringJobsList(){
|
||||||
|
if(!connectedToBackend)
|
||||||
|
return;
|
||||||
|
//console.debug("Updating MonitoringJobsList");
|
||||||
|
Job.GetMonitoringJobs(apiUri)
|
||||||
|
.then((jobs) => {
|
||||||
|
if(jobs.length > 0)
|
||||||
|
return Job.GetJobs(apiUri, jobs)
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
.then((jobs) => setMonitoringJobs(jobs));
|
||||||
|
}
|
||||||
|
|
||||||
|
function StartSearchMangaEntry() : ReactElement {
|
||||||
|
return (<div key="monitorMangaEntry.StartSearch" className="monitorMangaEntry" onClick={onStartSearch}>
|
||||||
|
<div className="Manga" key="StartSearch.Manga">
|
||||||
|
<img src="../media/blahaj.png" alt="Blahaj"></img>
|
||||||
|
<div>
|
||||||
|
<p style={{textAlign: "center", width: "100%"}} className="Manga-name">Add new Manga</p>
|
||||||
|
<p style={{fontSize: "42pt", textAlign: "center"}}>+</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteJob : MouseEventHandler = (e) => {
|
||||||
|
const jobId = e.currentTarget.id.slice(e.currentTarget.id.indexOf("-")+1);
|
||||||
|
//console.info(`Pressed ${e.currentTarget.id} => ${jobId}`);
|
||||||
|
Job.DeleteJob(apiUri, jobId).then(() => onJobsChanged(jobId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const StartJob : MouseEventHandler = (e) => {
|
||||||
|
const jobId = e.currentTarget.id.slice(e.currentTarget.id.indexOf("-")+1);
|
||||||
|
//console.info(`Pressed ${e.currentTarget.id} => ${jobId}`);
|
||||||
|
Job.StartJob(apiUri, jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="MonitorMangaList">
|
||||||
|
{StartSearchMangaEntry()}
|
||||||
|
{AllManga.map((manga: IManga) => {
|
||||||
|
const job = MonitoringJobs.find(job => job.mangaInternalId == manga.internalId);
|
||||||
|
if (job === undefined || job == null)
|
||||||
|
return <div>Error. Could not find matching job for {manga.internalId}</div>
|
||||||
|
return <div key={"monitorMangaEntry-" + manga.internalId} className="monitorMangaEntry">
|
||||||
|
{CoverCard(apiUri, manga)}
|
||||||
|
<div className="MangaActionButtons">
|
||||||
|
<div id={"Delete-"+job.id} className="DeleteJobButton" onClick={DeleteJob}><Icon path={mdiTrashCanOutline} size={1.5} /></div>
|
||||||
|
<div id={"Start-"+job.id} className="StartJobNowButton" onClick={StartJob}><Icon path={mdiPlayBoxOutline} size={1.5} /></div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</div>)
|
||||||
|
}
|
164
Website/modules/NotificationConnector.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import INotificationConnector from "./interfaces/INotificationConnector";
|
||||||
|
import {deleteData, getData, postData} from "../App";
|
||||||
|
|
||||||
|
export default abstract 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);
|
||||||
|
})
|
||||||
|
.catch(Promise.reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Test(apiUri: string) : Promise<boolean>;
|
||||||
|
public abstract Reset(apiUri: string) : Promise<boolean>;
|
||||||
|
public abstract Create(apiUri: string) : Promise<boolean>;
|
||||||
|
protected abstract CheckConnector() : boolean;
|
||||||
|
|
||||||
|
protected async TestConnector(apiUri: string, connectorType: string, data: object): Promise<boolean> {
|
||||||
|
if(!this.CheckConnector())
|
||||||
|
return Promise.reject("Connector not fully configured.");
|
||||||
|
//console.info(`Testing ${connectorType}`);
|
||||||
|
return postData(`${apiUri}/v2/NotificationConnector/${connectorType}/Test`, data)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Successfully tested ${connectorType}`);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(Promise.reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async ResetConnector(apiUri: string, connectorType: string): Promise<boolean> {
|
||||||
|
if(!this.CheckConnector())
|
||||||
|
return Promise.reject("Connector not fully configured.");
|
||||||
|
//console.info(`Deleting ${connectorType}`);
|
||||||
|
return deleteData(`${apiUri}/v2/NotificationConnector/${connectorType}`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Successfully deleted ${connectorType}`);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(Promise.reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async CreateConnector(apiUri: string, connectorType: string, data: object): Promise<boolean> {
|
||||||
|
if(!this.CheckConnector())
|
||||||
|
return Promise.reject("Connector not fully configured.");
|
||||||
|
//console.info(`Creating ${connectorType}`);
|
||||||
|
return postData(`${apiUri}/v2/NotificationConnector/${connectorType}`, data)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Successfully created ${connectorType}`);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(Promise.reject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Gotify extends NotificationConnector
|
||||||
|
{
|
||||||
|
public url = "";
|
||||||
|
private appToken = "";
|
||||||
|
|
||||||
|
constructor({url, appToken} : {url: string, appToken:string}){
|
||||||
|
super();
|
||||||
|
this.url = url;
|
||||||
|
this.appToken = appToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Test(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.TestConnector(apiUri, "Gotify", {url: this.url, appToken: this.appToken}).then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Reset(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.ResetConnector(apiUri, "Gotify").then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Create(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.CreateConnector(apiUri, "Gotify", {url: this.url, appToken: this.appToken}).then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CheckConnector(): boolean {
|
||||||
|
try{
|
||||||
|
new URL(this.url)
|
||||||
|
}catch{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(this.appToken.length < 1 || this.appToken.length < 1)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Lunasea extends NotificationConnector
|
||||||
|
{
|
||||||
|
private webhook = "";
|
||||||
|
|
||||||
|
constructor({webhook} : {webhook: string}){
|
||||||
|
super();
|
||||||
|
this.webhook = webhook;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Test(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.TestConnector(apiUri, "LunaSea", {webhook: this.webhook}).then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Reset(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.ResetConnector(apiUri, "LunaSea").then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Create(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.CreateConnector(apiUri, "LunaSea", {webhook: this.webhook}).then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CheckConnector(): boolean {
|
||||||
|
if(this.webhook.length < 1 || this.webhook.length < 1)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Ntfy extends NotificationConnector
|
||||||
|
{
|
||||||
|
public url = "";
|
||||||
|
private username = "";
|
||||||
|
private password = "";
|
||||||
|
public topic:string | undefined = undefined;
|
||||||
|
|
||||||
|
constructor({url, username, password, topic} : {url: string, username: string, password: string, topic : string | undefined}){
|
||||||
|
super();
|
||||||
|
this.url = url;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.topic = topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Test(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.TestConnector(apiUri, "Ntfy", {url: this.url, username: this.username, password: this.password, topic: this.topic}).then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Reset(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.ResetConnector(apiUri, "Ntfy").then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Create(apiUri: string) : Promise<boolean> {
|
||||||
|
return this.CreateConnector(apiUri, "Ntfy", {url: this.url, username: this.username, password: this.password, topic: this.topic}).then(() => true).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CheckConnector(): boolean {
|
||||||
|
try{
|
||||||
|
new URL(this.url)
|
||||||
|
}catch{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(this.username.length < 1 || this.password.length < 1)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
120
Website/modules/QueuePopUp.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import IJob from "./interfaces/IJob";
|
||||||
|
import '../styles/queuePopUp.css';
|
||||||
|
import '../styles/popup.css';
|
||||||
|
import Job from "./Job";
|
||||||
|
import IManga, {QueueItem} from "./interfaces/IManga";
|
||||||
|
import Manga from "./Manga";
|
||||||
|
|
||||||
|
export default function QueuePopUp({connectedToBackend, children, apiUri} : {connectedToBackend: boolean, children: JSX.Element[], apiUri: string}) {
|
||||||
|
|
||||||
|
const [StandbyJobs, setStandbyJobs] = React.useState<IJob[]>([]);
|
||||||
|
const [StandbyJobsManga, setStandbyJobsManga] = React.useState<IManga[]>([]);
|
||||||
|
const [RunningJobs, setRunningJobs] = React.useState<IJob[]>([]);
|
||||||
|
const [RunningJobsManga, setRunningJobsManga] = React.useState<IManga[]>([]);
|
||||||
|
const [showQueuePopup, setShowQueuePopup] = useState<boolean>(false);
|
||||||
|
const [queueListInterval, setQueueListInterval] = React.useState<number>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!showQueuePopup)
|
||||||
|
return;
|
||||||
|
UpdateMonitoringJobsList();
|
||||||
|
}, [showQueuePopup]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(connectedToBackend){
|
||||||
|
UpdateMonitoringJobsList();
|
||||||
|
setQueueListInterval(setInterval(() => {
|
||||||
|
UpdateMonitoringJobsList();
|
||||||
|
}, 2000));
|
||||||
|
}else{
|
||||||
|
clearInterval(queueListInterval);
|
||||||
|
setQueueListInterval(undefined);
|
||||||
|
}
|
||||||
|
}, [connectedToBackend]);
|
||||||
|
|
||||||
|
function UpdateMonitoringJobsList(){
|
||||||
|
Job.GetStandbyJobs(apiUri)
|
||||||
|
.then((jobs:string[]) => {
|
||||||
|
if(jobs.length > 0)
|
||||||
|
return Job.GetJobs(apiUri, jobs);
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
.then((jobs:IJob[]) => {
|
||||||
|
//console.debug("Removing Metadata Jobs");
|
||||||
|
//console.log(StandbyJobs)
|
||||||
|
setStandbyJobs(jobs.filter(job => job.jobType <= 2));
|
||||||
|
//console.log(StandbyJobs)
|
||||||
|
});
|
||||||
|
Job.GetRunningJobs(apiUri)
|
||||||
|
.then((jobs:string[]) => {
|
||||||
|
if(jobs.length > 0)
|
||||||
|
return Job.GetJobs(apiUri, jobs);
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
.then((jobs:IJob[]) =>{
|
||||||
|
//console.debug("Removing Metadata Jobs");
|
||||||
|
setRunningJobs(jobs.filter(job => job.jobType <= 2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(StandbyJobs.length < 1)
|
||||||
|
return;
|
||||||
|
const mangaIds = StandbyJobs.filter(job => job.jobType<=2).map((job) => job.mangaInternalId != undefined ? job.mangaInternalId : job.chapter != undefined ? job.chapter.parentManga.internalId : "");
|
||||||
|
//console.debug(`Waiting mangaIds: ${mangaIds.join(",")}`);
|
||||||
|
Manga.GetMangaByIds(apiUri, mangaIds)
|
||||||
|
.then(setStandbyJobsManga);
|
||||||
|
}, [StandbyJobs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(RunningJobs.length < 1)
|
||||||
|
return;
|
||||||
|
//console.log(RunningJobs);
|
||||||
|
const mangaIds = RunningJobs.filter(job => job.jobType<=2).map((job) => job.mangaInternalId != undefined ? job.mangaInternalId : job.chapter != undefined ? job.chapter.parentManga.internalId : "");
|
||||||
|
//console.debug(`Running mangaIds: ${mangaIds.join(",")}`);
|
||||||
|
Manga.GetMangaByIds(apiUri, mangaIds)
|
||||||
|
.then(setRunningJobsManga);
|
||||||
|
}, [RunningJobs]);
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<div onClick={() => setShowQueuePopup(true)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{showQueuePopup
|
||||||
|
? <div className="popup" id="QueuePopUp">
|
||||||
|
<div className="popupHeader">
|
||||||
|
<h1>Queue Status</h1>
|
||||||
|
<img alt="Close Search" className="close" src="../media/close-x.svg"
|
||||||
|
onClick={() => setShowQueuePopup(false)}/>
|
||||||
|
</div>
|
||||||
|
<div id="QueuePopUpBody" className="popupBody">
|
||||||
|
<div id="RunningJobQueue">
|
||||||
|
<h1>Running</h1>
|
||||||
|
<div className="JobQueue">
|
||||||
|
{RunningJobs.map((job: IJob) => {
|
||||||
|
const manga = RunningJobsManga.find(manga => manga.internalId == job.mangaInternalId || manga.internalId == job.chapter?.parentManga.internalId);
|
||||||
|
if (manga === undefined || manga === null)
|
||||||
|
return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga for {job.id}</div>
|
||||||
|
return QueueItem(apiUri, manga, job, UpdateMonitoringJobsList);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="WaitingJobQueue">
|
||||||
|
<h1>Standby</h1>
|
||||||
|
<div className="JobQueue">
|
||||||
|
{StandbyJobs.map((job: IJob) => {
|
||||||
|
const manga = StandbyJobsManga.find(manga => manga.internalId == job.mangaInternalId || manga.internalId == job.chapter?.parentManga.internalId);
|
||||||
|
if (manga === undefined || manga === null)
|
||||||
|
return <div key={"QueueJob-" + job.id}>Error. Could not find matching manga for {job.id}</div>
|
||||||
|
return QueueItem(apiUri, manga, job, UpdateMonitoringJobsList);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
111
Website/modules/Search.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, {ChangeEventHandler, EventHandler, useEffect, useState} from 'react';
|
||||||
|
import {MangaConnector} from "./MangaConnector";
|
||||||
|
import IMangaConnector from "./interfaces/IMangaConnector";
|
||||||
|
import {isValidUri} from "../App";
|
||||||
|
import IManga, {SearchResult} from "./interfaces/IManga";
|
||||||
|
import '../styles/search.css';
|
||||||
|
import '../styles/MangaSearchResult.css'
|
||||||
|
|
||||||
|
export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch} : {apiUri: string, jobInterval: Date, onJobsChanged: (internalId: string) => void, closeSearch(): void}) {
|
||||||
|
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(() => {
|
||||||
|
if(mangaConnectors === undefined) {
|
||||||
|
MangaConnector.GetAllConnectors().then(setConnectors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [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;
|
||||||
|
}
|
||||||
|
if(!isValidUri(searchBoxValue)){
|
||||||
|
MangaConnector.GetMangaFromConnectorByTitle(selectedConnector, searchBoxValue)
|
||||||
|
.then((mangas: IManga[]) => {
|
||||||
|
setSearchResults(mangas);
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
MangaConnector.GetMangaFromConnectorByUrl(selectedConnector, searchBoxValue)
|
||||||
|
.then((manga: IManga) => {
|
||||||
|
setSearchResults([manga]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeSelectedLanguage : ChangeEventHandler<HTMLSelectElement> = (event) => setSelectedLanguage(event.target.value);
|
||||||
|
|
||||||
|
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}></input>
|
||||||
|
<select id="Searchbox-Connector" value={selectedConnector === undefined ? "" : selectedConnector.name} onChange={selectedConnectorChanged}>
|
||||||
|
<option value="" disabled hidden>Select</option>
|
||||||
|
{mangaConnectors === undefined
|
||||||
|
? <option value="Loading">Loading</option>
|
||||||
|
: mangaConnectors.map(con => <option value={con.name} key={con.name}>{con.name}</option>)}
|
||||||
|
</select>
|
||||||
|
<select id="Searchbox-language" onChange={changeSelectedLanguage} value={selectedLanguage === null ? "" : selectedLanguage}>
|
||||||
|
{selectedConnector === undefined
|
||||||
|
? <option value="" disabled hidden>Select Connector</option>
|
||||||
|
: selectedConnector.SupportedLanguages.map(language => <option value={language} key={language}>{language}</option>)}
|
||||||
|
</select>
|
||||||
|
<button id="Searchbox-button" type="submit" onClick={ExecuteSearch}>Search</button>
|
||||||
|
</div>
|
||||||
|
<img alt="Close Search" id="closeSearch" src="../media/close-x.svg" onClick={closeSearch} />
|
||||||
|
<div id="SearchResults">
|
||||||
|
{searchResults === undefined
|
||||||
|
? <p></p>
|
||||||
|
: searchResults.map(result => SearchResult(apiUri, result, jobInterval, onJobsChanged))}
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
318
Website/modules/Settings.tsx
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
import React, {ChangeEventHandler, KeyboardEventHandler, MouseEventHandler, useEffect, useState} from 'react';
|
||||||
|
import IFrontendSettings from "./interfaces/IFrontendSettings";
|
||||||
|
import '../styles/settings.css';
|
||||||
|
import IBackendSettings from "./interfaces/IBackendSettings";
|
||||||
|
import {getData, postData} from "../App";
|
||||||
|
import LibraryConnector, {Kavita, Komga} from "./LibraryConnector";
|
||||||
|
import NotificationConnector, {Gotify, Lunasea, Ntfy} from "./NotificationConnector";
|
||||||
|
import ILibraryConnector from "./interfaces/ILibraryConnector";
|
||||||
|
import INotificationConnector from "./interfaces/INotificationConnector";
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
import '../styles/react-toggle.css';
|
||||||
|
|
||||||
|
export default function Settings({backendConnected, apiUri, settings, changeSettings} : {backendConnected: boolean, apiUri: string, settings: IFrontendSettings, changeSettings: (settings: IFrontendSettings) => void}) {
|
||||||
|
const [frontendSettings, setFrontendSettings] = useState<IFrontendSettings>(settings);
|
||||||
|
const [backendSettings, setBackendSettings] = useState<IBackendSettings>();
|
||||||
|
const [showSettings, setShowSettings] = useState<boolean>(false);
|
||||||
|
const [libraryConnectors, setLibraryConnectors] = useState<ILibraryConnector[]>();
|
||||||
|
const [notificationConnectors, setNotificationConnectors] = useState<INotificationConnector[]>();
|
||||||
|
const [komgaSettings, setKomgaSettings] = useState<{url: string, username: string, password: string}>({url: "", username: "", password: ""});
|
||||||
|
const [kavitaSettings, setKavitaSettings] = useState<{url: string, username: string, password: string}>({url: "", username: "", password: ""});
|
||||||
|
const [gotifySettings, setGotifySettings] = useState<{url: string, appToken: string}>({url: "", appToken: ""});
|
||||||
|
const [lunaseaSettings, setLunaseaSettings] = useState<{webhook: string}>({webhook: ""});
|
||||||
|
const [ntfySettings, setNtfySettings] = useState<{url: string, username: string, password: string, topic: string | undefined}>({url: "", username: "", password: "", topic: undefined});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.debug(`${showSettings ? "Showing" : "Not showing"} settings.`);
|
||||||
|
if(!showSettings || !backendConnected)
|
||||||
|
return;
|
||||||
|
UpdateBackendSettings();
|
||||||
|
LibraryConnector.GetLibraryConnectors(apiUri).then(setLibraryConnectors).catch(console.error);
|
||||||
|
NotificationConnector.GetNotificationConnectors(apiUri).then(setNotificationConnectors).catch(console.error);
|
||||||
|
}, [showSettings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
changeSettings(frontendSettings);
|
||||||
|
}, [frontendSettings]);
|
||||||
|
|
||||||
|
async function GetSettings(apiUri: string) : Promise<IBackendSettings> {
|
||||||
|
//console.info("Getting Settings");
|
||||||
|
return getData(`${apiUri}/v2/Settings`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info("Got Settings");
|
||||||
|
const ret = json as IBackendSettings;
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
})
|
||||||
|
.catch(Promise.reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateBackendSettings() {
|
||||||
|
GetSettings(apiUri).then(setBackendSettings).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetKomga = () : ILibraryConnector | undefined =>
|
||||||
|
libraryConnectors?.find(con => con.libraryType == 0);
|
||||||
|
|
||||||
|
const KomgaConnected = () : boolean => GetKomga() != undefined;
|
||||||
|
|
||||||
|
const GetKavita = () : ILibraryConnector | undefined =>
|
||||||
|
libraryConnectors?.find(con => con.libraryType == 1);
|
||||||
|
|
||||||
|
const KavitaConnected = () : boolean => GetKavita() != undefined;
|
||||||
|
|
||||||
|
const GetGotify = () : INotificationConnector | undefined =>
|
||||||
|
notificationConnectors?.find(con => con.notificationConnectorType == 0);
|
||||||
|
|
||||||
|
const GotifyConnected = () : boolean => GetGotify() != undefined;
|
||||||
|
|
||||||
|
const GetLunasea = () : INotificationConnector | undefined =>
|
||||||
|
notificationConnectors?.find(con => con.notificationConnectorType == 1);
|
||||||
|
|
||||||
|
const LunaseaConnected = () : boolean => GetLunasea() != undefined;
|
||||||
|
|
||||||
|
const GetNtfy = () : INotificationConnector | undefined =>
|
||||||
|
notificationConnectors?.find(con => con.notificationConnectorType == 2);
|
||||||
|
|
||||||
|
const NtfyConnected = () : boolean => GetNtfy() != undefined;
|
||||||
|
|
||||||
|
const SubmitApiUri : KeyboardEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
if(e.currentTarget.value.length < 1)
|
||||||
|
return;
|
||||||
|
if(e.key == "Enter"){
|
||||||
|
setFrontendSettings({...frontendSettings, apiUri: e.currentTarget.value});
|
||||||
|
RefreshInputs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubmitUserAgent: KeyboardEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
if(e.currentTarget.value.length < 1 || backendSettings === undefined)
|
||||||
|
return;
|
||||||
|
if(e.key == "Enter"){
|
||||||
|
//console.info(`Updating Useragent ${e.currentTarget.value}`);
|
||||||
|
postData(`${apiUri}/v2/Settings/UserAgent`, {value: e.currentTarget.value})
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Successfully updated Useragent ${e.currentTarget.value}`);
|
||||||
|
UpdateBackendSettings();
|
||||||
|
RefreshInputs();
|
||||||
|
})
|
||||||
|
.catch(() => alert("Failed to update Useragent."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResetUserAgent: MouseEventHandler<HTMLSpanElement> = () => {
|
||||||
|
//console.info(`Resetting Useragent`);
|
||||||
|
postData(`${apiUri}/v2/Settings/UserAgent`, {value: undefined})
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Successfully reset Useragent`);
|
||||||
|
UpdateBackendSettings();
|
||||||
|
RefreshInputs();
|
||||||
|
})
|
||||||
|
.catch(() => alert("Failed to update Useragent."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const SetAprilFoolsMode : ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
//console.info(`Updating AprilFoolsMode ${e.target.checked}`);
|
||||||
|
postData(`${apiUri}/v2/Settings/AprilFoolsMode`, {value: e.target.checked})
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Successfully updated AprilFoolsMode ${e.currentTarget.value}`);
|
||||||
|
UpdateBackendSettings();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const SetCompressImages : MouseEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
//console.info(`Updating ImageCompression ${e.currentTarget.value}`);
|
||||||
|
postData(`${apiUri}/v2/Settings/CompressImages`, {value: e.currentTarget.value})
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Successfully updated ImageCompression ${e.currentTarget.value}`);
|
||||||
|
UpdateBackendSettings();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const SetBWImages : ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
//console.info(`Updating B/W Images ${e.target.checked}`);
|
||||||
|
postData(`${apiUri}/v2/Settings/BWImages`, {value: e.target.checked})
|
||||||
|
.then((json) => {
|
||||||
|
//console.info(`Successfully updated B/W Images ${e.target.checked}`);
|
||||||
|
UpdateBackendSettings();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function RefreshInputs(){
|
||||||
|
alert("Saved.");
|
||||||
|
setShowSettings(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="Settings">
|
||||||
|
<img id="SettingsIcon" src="../media/settings-cogwheel.svg" alt="cogwheel" onClick={() => setShowSettings(true)}/>
|
||||||
|
{showSettings
|
||||||
|
? <div className="popup">
|
||||||
|
<div className="popupHeader">
|
||||||
|
<h1>Settings</h1>
|
||||||
|
<img alt="Close Settings" className="close" src="../media/close-x.svg" onClick={() => setShowSettings(false)}/>
|
||||||
|
</div>
|
||||||
|
<div id="settingsPopupBody" className="popupBody">
|
||||||
|
<div className="settings-section">
|
||||||
|
TRANGA
|
||||||
|
<div className="settings-section-content">
|
||||||
|
<div className="section-item">
|
||||||
|
<span className="settings-section-title">API Settings</span>
|
||||||
|
<label className="tooltip" data-tooltip="Set the URI of the Backend. Include https:// and if necessary port." htmlFor="settingApiUri">API URI:</label>
|
||||||
|
<input placeholder={frontendSettings.apiUri} type="text" id="settingApiUri"
|
||||||
|
onKeyDown={SubmitApiUri}/>
|
||||||
|
<label htmlFor="userAgent" className="tooltip" data-tooltip="Set a custom User-Agent. This will allow more frequent requests to sites.">User Agent:</label>
|
||||||
|
<input id="userAgent" type="text"
|
||||||
|
placeholder={backendSettings != undefined ? backendSettings.userAgent : "UserAgent"}
|
||||||
|
onKeyDown={SubmitUserAgent}/>
|
||||||
|
<span id="resetUserAgent" onClick={ResetUserAgent}>Reset</span>
|
||||||
|
<label htmlFor="aprilFoolsMode" className="tooltip" data-tooltip="Disable all downloads on April 1st">April Fools Mode</label>
|
||||||
|
<Toggle id="aprilFoolsMode"
|
||||||
|
checked={backendSettings?.aprilFoolsMode ?? false}
|
||||||
|
onChange={SetAprilFoolsMode}/>
|
||||||
|
<label htmlFor="compression" className="tooltip" data-tooltip="JPEG Compression Quality">Image Compression</label>
|
||||||
|
<input type="range" min="1" max="100" defaultValue={backendSettings?.compression ?? 50} className="slider" id="compression" onMouseUp={SetCompressImages}/>
|
||||||
|
<label htmlFor="bwImages">B/W Images</label>
|
||||||
|
<Toggle id="bwImages"
|
||||||
|
checked={backendSettings?.bwImages ?? false}
|
||||||
|
onChange={SetBWImages}/>
|
||||||
|
</div>
|
||||||
|
<div className="section-item">
|
||||||
|
<span className="settings-section-title">Rate Limits</span>
|
||||||
|
<label htmlFor="DefaultRL">Default:</label>
|
||||||
|
<input id="defaultRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.Default.toString() : "-1"} />
|
||||||
|
<label htmlFor="CoverRL">Manga Covers:</label>
|
||||||
|
<input id="coverRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaCover.toString() : "-1"} />
|
||||||
|
<label htmlFor="ImageRL">Manga Images:</label>
|
||||||
|
<input id="imageRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaImage.toString() : "-1"} />
|
||||||
|
<label htmlFor="InfoRL">Manga Info:</label>
|
||||||
|
<input id="infoRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaInfo.toString() : "-1"} />
|
||||||
|
</div>
|
||||||
|
<div className="section-item">
|
||||||
|
<span className="settings-section-title">Appearance</span>
|
||||||
|
<label htmlFor="cssStyle">Library Style:</label>
|
||||||
|
<select id="cssStyle">
|
||||||
|
<option id="card_compact" value="card_compact">Cards (Compact)</option>
|
||||||
|
<option id="card_hover" value="card_hover">Cards (Hover)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-section">
|
||||||
|
<span className="settings-section-title">Sources</span>
|
||||||
|
<div className="settings-section-content">
|
||||||
|
<div className="section-item">
|
||||||
|
<span className="settings-section-title">
|
||||||
|
<img src="../media/connector-icons/mangadex-logo.svg" alt="Mangadex Logo" />
|
||||||
|
<a href="https://mangadex.org">MangaDex</a>
|
||||||
|
</span>
|
||||||
|
<label htmlFor="mDexFeedRL">Feed Rate Limit:</label>
|
||||||
|
<input id="mDexFeedRL" type="text" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaDexFeed.toString() : "-1"} />
|
||||||
|
<label htmlFor="mDexImageRL">Image Rate Limit:</label>
|
||||||
|
<input id="mDexImageRL" type="number" placeholder={backendSettings != undefined ? backendSettings.requestLimits.MangaDexImage.toString() : "-1"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-section" >
|
||||||
|
LIBRARY CONNECTORS
|
||||||
|
<div className="settings-section-content">
|
||||||
|
<div className="section-item" connector-status={KomgaConnected() ? "Configured" : "Not Configured"}>
|
||||||
|
<span className="settings-section-title">
|
||||||
|
<img src='../media/connector-icons/komga.svg' alt="Komga Logo"/>
|
||||||
|
Komga
|
||||||
|
</span>
|
||||||
|
<label htmlFor="komgaUrl">URL</label>
|
||||||
|
<input placeholder={GetKomga()?.baseUrl ?? "URL"} id="komgaUrl" type="text" onChange={(e) => setKomgaSettings(s => ({...s, url: e.target.value}))} />
|
||||||
|
<label htmlFor="komgaUsername">Username</label>
|
||||||
|
<input placeholder={KomgaConnected() ? "***" : "Username"} id="komgaUsername" type="text" onChange={(e) => setKomgaSettings(s => ({...s, username: e.target.value}))} />
|
||||||
|
<label htmlFor="komgaPassword">Password</label>
|
||||||
|
<input placeholder={KomgaConnected() ? "***" : "Password"} id="komgaPassword" type="password" onChange={(e) => setKomgaSettings(s => ({...s, password: e.target.value}))} />
|
||||||
|
<div className="section-actions">
|
||||||
|
<span onClick={() => new Komga(komgaSettings).Test(apiUri).then(()=>alert("Test successful"))}>Test</span>
|
||||||
|
<span onClick={() => new Komga(komgaSettings).Reset(apiUri).then(RefreshInputs)}>Reset</span>
|
||||||
|
<span onClick={() => new Komga(komgaSettings).Create(apiUri).then(RefreshInputs)}>Apply</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="section-item" connector-status={KavitaConnected() ? "Configured" : "Not Configured" }>
|
||||||
|
<span className="settings-section-title">
|
||||||
|
<img src='../media/connector-icons/kavita.png' alt="Kavita Logo"/>
|
||||||
|
Kavita
|
||||||
|
</span>
|
||||||
|
<label htmlFor="kavitaUrl">URL</label>
|
||||||
|
<input placeholder={GetKavita()?.baseUrl ?? "URL"} id="kavitaUrl" type="text" onChange={(e) => setKavitaSettings(s => ({...s, url: e.target.value}))} />
|
||||||
|
<label htmlFor="kavitaUsername">Username</label>
|
||||||
|
<input placeholder={KavitaConnected() ? "***" : "Username"} id="kavitaUsername" type="text" onChange={(e) => setKavitaSettings(s => ({...s, username: e.target.value}))} />
|
||||||
|
<label htmlFor="kavitaPassword">Password</label>
|
||||||
|
<input placeholder={KavitaConnected() ? "***" : "Password"} id="kavitaPassword" type="password" onChange={(e) => setKavitaSettings(s => ({...s, password: e.target.value}))} />
|
||||||
|
<div className="section-actions">
|
||||||
|
<span onClick={() => new Kavita(kavitaSettings).Test(apiUri).then(()=>alert("Test successful"))}>Test</span>
|
||||||
|
<span onClick={() => new Kavita(kavitaSettings).Reset(apiUri).then(RefreshInputs)}>Reset</span>
|
||||||
|
<span onClick={() => new Kavita(kavitaSettings).Create(apiUri).then(RefreshInputs)}>Apply</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-section">
|
||||||
|
NOTIFICATION CONNECTORS
|
||||||
|
<div className="settings-section-content">
|
||||||
|
<div className="section-item" connector-status={GotifyConnected() ? "Configured" : "Not Configured"}>
|
||||||
|
<span className="settings-section-title">
|
||||||
|
<img src='../media/connector-icons/gotify-logo.png' alt="Gotify Logo"/>
|
||||||
|
Gotify
|
||||||
|
</span>
|
||||||
|
<label htmlFor="gotifyUrl">URL</label>
|
||||||
|
<input placeholder={GetGotify()?.endpoint ?? "URL"} id="gotifyUrl" type="text" onChange={(e) => setGotifySettings(s => ({...s, url: e.target.value}))} />
|
||||||
|
<label htmlFor="gotifyAppToken">AppToken</label>
|
||||||
|
<input placeholder={GotifyConnected() ? "***" : "AppToken"} id="gotifyAppToken" type="text" onChange={(e) => setGotifySettings(s => ({...s, appToken: e.target.value}))} />
|
||||||
|
<div className="section-actions">
|
||||||
|
<span onClick={() => new Gotify(gotifySettings).Test(apiUri).then(()=>alert("Test successful"))}>Test</span>
|
||||||
|
<span onClick={() => new Gotify(gotifySettings).Reset(apiUri).then(RefreshInputs)}>Reset</span>
|
||||||
|
<span onClick={() => new Gotify(gotifySettings).Create(apiUri).then(RefreshInputs)}>Apply</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="section-item"
|
||||||
|
connector-status={LunaseaConnected() ? "Configured" : "Not Configured"}>
|
||||||
|
<span className="settings-section-title">
|
||||||
|
<img src='../media/connector-icons/lunasea.png' alt="Lunasea Logo"/>
|
||||||
|
LunaSea
|
||||||
|
</span>
|
||||||
|
<label htmlFor="lunaseaWebhook">Webhook id</label>
|
||||||
|
<input placeholder={GetLunasea() != undefined ? "***" : "device/:id or user/:id"} id="lunaseaWebhook" type="text" onChange={(e) => setLunaseaSettings(s => ({...s, webhook: e.target.value}))} />
|
||||||
|
<div className="section-actions">
|
||||||
|
<span onClick={() => new Lunasea(lunaseaSettings).Test(apiUri).then(()=>alert("Test successful"))}>Test</span>
|
||||||
|
<span onClick={() => new Lunasea(lunaseaSettings).Reset(apiUri).then(RefreshInputs)}>Reset</span>
|
||||||
|
<span onClick={() => new Lunasea(lunaseaSettings).Create(apiUri).then(RefreshInputs)}>Apply</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="section-item"
|
||||||
|
connector-status={NtfyConnected() ? "Configured" : "Not Configured"}>
|
||||||
|
<span className="settings-section-title">
|
||||||
|
<img src='../media/connector-icons/ntfy.svg' alt="ntfy Logo"/>
|
||||||
|
Ntfy
|
||||||
|
</span>
|
||||||
|
<label htmlFor="ntfyEndpoint">URL</label>
|
||||||
|
<input placeholder={GetNtfy()?.endpoint ?? "URL"} id="ntfyEndpoint" type="text" onChange={(e) => setNtfySettings(s => ({...s, url: e.target.value}))} />
|
||||||
|
<label htmlFor="ntfyUsername">Username</label>
|
||||||
|
<input placeholder={NtfyConnected() ? "***" : "Username"} id="ntfyUsername" type="text" onChange={(e) => setNtfySettings(s => ({...s, username: e.target.value}))} />
|
||||||
|
<label htmlFor="ntfyPassword">Password</label>
|
||||||
|
<input placeholder={NtfyConnected() ? "***" : "Password"} id="ntfyPassword" type="password" onChange={(e) => setNtfySettings(s => ({...s, password: e.target.value}))} />
|
||||||
|
<label htmlFor="ntfyTopic">Topic</label>
|
||||||
|
<input placeholder={GetNtfy()?.topic ?? "Topic"} id="ntfyTopic" type="text" onChange={(e) => setNtfySettings(s => ({...s, topic: e.target.value}))} />
|
||||||
|
<div className="section-actions">
|
||||||
|
<span onClick={() => new Ntfy(ntfySettings).Test(apiUri).then(()=>alert("Test successful"))}>Test</span>
|
||||||
|
<span onClick={() => new Ntfy(ntfySettings).Reset(apiUri).then(RefreshInputs)}>Reset</span>
|
||||||
|
<span onClick={() => new Ntfy(ntfySettings).Create(apiUri).then(RefreshInputs)}>Apply</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
20
Website/modules/interfaces/IBackendSettings.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export default interface IBackendSettings {
|
||||||
|
"downloadLocation": string;
|
||||||
|
"workingDirectory": string;
|
||||||
|
"apiPortNumber": number;
|
||||||
|
"userAgent": string;
|
||||||
|
"bufferLibraryUpdates": boolean;
|
||||||
|
"bufferNotifications": boolean;
|
||||||
|
"version": number;
|
||||||
|
"aprilFoolsMode": boolean;
|
||||||
|
"compression": number;
|
||||||
|
"bwImages": boolean;
|
||||||
|
"requestLimits": {
|
||||||
|
"MangaInfo": number;
|
||||||
|
"MangaDexFeed": number;
|
||||||
|
"MangaDexImage": number;
|
||||||
|
"MangaImage": number;
|
||||||
|
"MangaCover": number;
|
||||||
|
"Default": number
|
||||||
|
}
|
||||||
|
}
|
10
Website/modules/interfaces/IChapter.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import IManga from "./IManga";
|
||||||
|
|
||||||
|
export default interface IChapter{
|
||||||
|
parentManga: IManga;
|
||||||
|
name: string | undefined;
|
||||||
|
volumeNumber: string;
|
||||||
|
chapterNumber: string;
|
||||||
|
url: string;
|
||||||
|
fileName: string;
|
||||||
|
}
|
18
Website/modules/interfaces/IFrontendSettings.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {Cookies} from "react-cookie";
|
||||||
|
|
||||||
|
export default interface IFrontendSettings {
|
||||||
|
jobInterval: Date;
|
||||||
|
apiUri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadFrontendSettings(): IFrontendSettings {
|
||||||
|
const cookies = new Cookies();
|
||||||
|
return {
|
||||||
|
jobInterval: cookies.get('jobInterval') === undefined
|
||||||
|
? new Date(0,0,0,3)
|
||||||
|
: cookies.get('jobInterval'),
|
||||||
|
apiUri: cookies.get('apiUri') === undefined
|
||||||
|
? `${window.location.protocol}//${window.location.host}/api`
|
||||||
|
: cookies.get('apiUri')
|
||||||
|
}
|
||||||
|
}
|
28
Website/modules/interfaces/IJob.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import IMangaConnector from "./IMangaConnector";
|
||||||
|
import IProgressToken from "./IProgressToken";
|
||||||
|
import IChapter from "./IChapter";
|
||||||
|
|
||||||
|
export default interface IJob{
|
||||||
|
progressToken: IProgressToken;
|
||||||
|
recurring: boolean;
|
||||||
|
recurrenceTime: string;
|
||||||
|
lastExecution: Date;
|
||||||
|
nextExecution: Date;
|
||||||
|
id: string;
|
||||||
|
jobType: number;
|
||||||
|
parentJobId: string | null;
|
||||||
|
mangaConnector: IMangaConnector;
|
||||||
|
mangaInternalId: string | undefined; //only on DownloadNewChapters
|
||||||
|
translatedLanguage: string | undefined; //only on DownloadNewChapters
|
||||||
|
chapter: IChapter | undefined; //only on DownloadChapter
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JobTypeFromNumber(n: number): string {
|
||||||
|
switch(n) {
|
||||||
|
case 0: return "Download Chapter";
|
||||||
|
case 1: return "Download New Chapters";
|
||||||
|
case 2: return "Update Metadata";
|
||||||
|
case 3: return "Monitor";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
13
Website/modules/interfaces/ILibraryConnector.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export default interface ILibraryConnector {
|
||||||
|
libraryType: number;
|
||||||
|
baseUrl: string;
|
||||||
|
auth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetLibraryConnectorNameFromNumber(n: number): string {
|
||||||
|
switch(n){
|
||||||
|
case 0: return "Komga";
|
||||||
|
case 1: return "Kavita";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
130
Website/modules/interfaces/IManga.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import IMangaConnector from "./IMangaConnector";
|
||||||
|
import KeyValuePair from "./KeyValuePair";
|
||||||
|
import Manga from "../Manga";
|
||||||
|
import React, {EventHandler, ReactElement, ReactEventHandler} from "react";
|
||||||
|
import Icon from '@mdi/react';
|
||||||
|
import { mdiTagTextOutline, mdiAccountEdit } from '@mdi/js';
|
||||||
|
import MarkdownPreview from '@uiw/react-markdown-preview';
|
||||||
|
import IJob, {JobTypeFromNumber} from "./IJob";
|
||||||
|
import Job from "../Job";
|
||||||
|
import ProgressBar from "@ramonak/react-progress-bar";
|
||||||
|
|
||||||
|
export default interface IManga{
|
||||||
|
"sortName": string,
|
||||||
|
"authors": string[],
|
||||||
|
"altTitles": KeyValuePair[],
|
||||||
|
"description": string,
|
||||||
|
"tags": string[],
|
||||||
|
"coverUrl": string,
|
||||||
|
"coverFileNameInCache": string,
|
||||||
|
"links": KeyValuePair[],
|
||||||
|
"year": number,
|
||||||
|
"originalLanguage": string,
|
||||||
|
"releaseStatus": number,
|
||||||
|
"folderName": string,
|
||||||
|
"publicationId": string,
|
||||||
|
"internalId": string,
|
||||||
|
"ignoreChaptersBelow": number,
|
||||||
|
"latestChapterDownloaded": number,
|
||||||
|
"latestChapterAvailable": number,
|
||||||
|
"websiteUrl": string,
|
||||||
|
"mangaConnector": IMangaConnector
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReleaseStatusFromNumber(n: number): string {
|
||||||
|
switch(n) {
|
||||||
|
case 0: return "Ongoing";
|
||||||
|
case 1: return "Completed";
|
||||||
|
case 2: return "OnHiatus";
|
||||||
|
case 3: return "Cancelled";
|
||||||
|
case 4: return "Unreleased";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function CoverCard(apiUri: string, manga: IManga) : ReactElement {
|
||||||
|
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
|
||||||
|
if(e.currentTarget.src != Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget))
|
||||||
|
e.currentTarget.src = Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div className="Manga" key={manga.internalId}>
|
||||||
|
<img src="../../media/blahaj.png" onLoad={MangaCover} alt="Manga Cover"></img>
|
||||||
|
<div>
|
||||||
|
<p className="pill connector-name">{manga.mangaConnector.name}</p>
|
||||||
|
<div className="Manga-status" release-status={ReleaseStatusFromNumber(manga.releaseStatus)}></div>
|
||||||
|
<p className="Manga-name">{manga.sortName}</p>
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchResult(apiUri: string, manga: IManga, interval: Date, onJobsChanged: (internalId: string) => void) : ReactElement {
|
||||||
|
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
|
||||||
|
if(e.currentTarget.src != Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget))
|
||||||
|
e.currentTarget.src = Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div className="SearchResult" key={manga.internalId}>
|
||||||
|
<img src="../../media/blahaj.png" onLoad={MangaCover} alt="Manga Cover"></img>
|
||||||
|
<p className="connector-name">{manga.mangaConnector.name}</p>
|
||||||
|
<div className="Manga-status" release-status={ReleaseStatusFromNumber(manga.releaseStatus)}></div>
|
||||||
|
<p className="Manga-name"><a href={manga.websiteUrl}>{manga.sortName}<img src="../../media/link.svg"
|
||||||
|
alt=""/></a></p>
|
||||||
|
<div className="Manga-tags">
|
||||||
|
{manga.authors.map(author => <p className="Manga-author" key={manga.internalId + "-author-" + author}>
|
||||||
|
<Icon path={mdiAccountEdit} size={0.5}/> {author}</p>)}
|
||||||
|
{manga.tags.map(tag => <p className="Manga-tag" key={manga.internalId + "-tag-" + tag}><Icon
|
||||||
|
path={mdiTagTextOutline} size={0.5}/> {tag}</p>)}
|
||||||
|
</div>
|
||||||
|
<MarkdownPreview className="Manga-description" source={manga.description}
|
||||||
|
style={{backgroundColor: "transparent", color: "black"}}/>
|
||||||
|
<button className="Manga-AddButton" onClick={() => {
|
||||||
|
Job.CreateJobDateInterval(apiUri, manga.internalId, "MonitorManga", interval).then(() => onJobsChanged(manga.internalId));
|
||||||
|
}}>Monitor
|
||||||
|
</button>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProgressbarStr(job: IJob): string {
|
||||||
|
return job.progressToken.timeRemaining.substring(0,job.progressToken.timeRemaining.indexOf(".")).concat(" ", ToPercentString(job.progressToken.progress));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToPercentString(n: number): string {
|
||||||
|
return n.toString().substring(2,4).concat("%");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QueueItem(apiUri: string, manga: IManga, job: IJob, triggerUpdate: () => void){
|
||||||
|
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
|
||||||
|
if(e.currentTarget.src != Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget))
|
||||||
|
e.currentTarget.src = Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="QueueJob" key={"QueueJob-" + job.id}>
|
||||||
|
<img src="../../media/blahaj.png" onLoad={MangaCover} alt="Manga Cover"></img>
|
||||||
|
<p className="QueueJob-Name">{manga.sortName}</p>
|
||||||
|
<p className="QueueJob-JobType">{JobTypeFromNumber(job.jobType)}</p>
|
||||||
|
<p className="QueueJob-additional">{job.jobType == 0 ? `Vol.${job.chapter?.volumeNumber} Ch.${job.chapter?.chapterNumber}` : ""}</p>
|
||||||
|
{job.progressToken.state === 0
|
||||||
|
? <ProgressBar labelColor={"#000"} height={"10px"} labelAlignment={"outside"}
|
||||||
|
className="QueueJob-Progressbar" completed={job.progressToken.progress} maxCompleted={1}
|
||||||
|
customLabel={ProgressbarStr(job)}/>
|
||||||
|
: <div className="QueueJob-Progressbar"></div>}
|
||||||
|
<div className="QueueJob-actions">
|
||||||
|
<button className="QueueJob-Cancel"
|
||||||
|
onClick={() => Job.CancelJob(apiUri, job.id).then(triggerUpdate)}>Cancel
|
||||||
|
</button>
|
||||||
|
{job.parentJobId != null
|
||||||
|
? <button className="QueueJob-Cancel"
|
||||||
|
onClick={() => Job.CancelJob(apiUri, job.parentJobId!).then(triggerUpdate)}>Cancel all
|
||||||
|
related</button>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
5
Website/modules/interfaces/IMangaConnector.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default interface IMangaConnector {
|
||||||
|
SupportedLanguages: string[];
|
||||||
|
name: string;
|
||||||
|
BaseUris: string[];
|
||||||
|
}
|
8
Website/modules/interfaces/INotificationConnector.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default interface INotificationConnector {
|
||||||
|
"notificationConnectorType": number; //see NotificationConnectorType
|
||||||
|
"endpoint": string | undefined;//only on Ntfy, Gotify
|
||||||
|
"appToken": string | undefined;//only on Gotify
|
||||||
|
"auth": string | undefined;//only on Ntfy
|
||||||
|
"topic": string | undefined;//only on Ntfy
|
||||||
|
"id": string | undefined;//only on LunaSea
|
||||||
|
}
|
21
Website/modules/interfaces/IProgressToken.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export default interface IProgressToken{
|
||||||
|
cancellationRequested: boolean;
|
||||||
|
increments: number;
|
||||||
|
incrementsCompleted: number;
|
||||||
|
progress: number;
|
||||||
|
lastUpdate: Date;
|
||||||
|
executionStarted: Date;
|
||||||
|
timeRemaining: string;
|
||||||
|
state: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetProgressStateFromNumber(n: number): string {
|
||||||
|
switch (n){
|
||||||
|
case 0: return "Running";
|
||||||
|
case 1: return "Complete";
|
||||||
|
case 2: return "Standby";
|
||||||
|
case 3: return "Cancelled";
|
||||||
|
case 4: return "Waiting";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
4
Website/modules/interfaces/KeyValuePair.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default interface KeyValuePair {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
@ -1,24 +1,3 @@
|
|||||||
#addPublication {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
width: 180px;
|
|
||||||
height: 300px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 10px 10px;
|
|
||||||
padding: 15px 20px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#addPublication p{
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 150pt;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 300px;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
height: 14pt;
|
height: 14pt;
|
||||||
@ -27,64 +6,40 @@
|
|||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
padding: 2pt 17px;
|
padding: 2pt 17px;
|
||||||
color: black;
|
color: black;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication{
|
.Manga{
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--secondary-color);
|
background-color: var(--secondary-color);
|
||||||
width: 180px;
|
width: 180px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 10px 10px;
|
margin: 10px 10px;
|
||||||
padding: 15px 19px;
|
padding: 14px 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication::after{
|
.Manga::after{
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0; top: 0;
|
left: 0; top: 0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
width: 100%; height: 100%;
|
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));
|
background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-information {
|
.Manga-name{
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-information * {
|
|
||||||
z-index: 1;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-details * {
|
|
||||||
z-index: 1;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
connector-name{
|
|
||||||
width: fit-content;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-name{
|
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
font-size: 16pt;
|
font-size: 16pt;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-status {
|
.Manga-status {
|
||||||
display:block;
|
display:block;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
@ -97,7 +52,7 @@ publication-status {
|
|||||||
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 10px, rgb(51, 51, 51) 0px 0px 10px 3px;
|
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 10px, rgb(51, 51, 51) 0px 0px 10px 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-status::after {
|
.Manga-status::after {
|
||||||
content: attr(release-status);
|
content: attr(release-status);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -118,42 +73,55 @@ publication-status::after {
|
|||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-status:hover::after{
|
.Manga-status:hover::after{
|
||||||
visibility:visible;
|
visibility:visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
publication-status[release-status="Ongoing"]{
|
.Manga-status[release-status="Ongoing"]{
|
||||||
background-color: limegreen;
|
background-color: limegreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-status[release-status="Completed"]{
|
.Manga-status[release-status="Completed"]{
|
||||||
background-color: blueviolet;
|
background-color: blueviolet;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-status[release-status="On Hiatus"]{
|
.Manga-status[release-status="On Hiatus"]{
|
||||||
background-color: darkorange;
|
background-color: darkorange;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-status[release-status="Cancelled"]{
|
.Manga-status[release-status="Cancelled"]{
|
||||||
background-color: firebrick;
|
background-color: firebrick;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-status[release-status="Upcoming"]{
|
.Manga-status[release-status="Upcoming"]{
|
||||||
background-color: aqua;
|
background-color: aqua;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication-status[release-status="Status Unavailable"]{
|
.Manga-status[release-status="Status Unavailable"]{
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
publication img {
|
.Manga img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
z-index: 0;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Manga p {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Manga > div {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
119
Website/styles/MangaSearchResult.css
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
.SearchResult {
|
||||||
|
background-color: var(--second-background-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 5px 5px 9px 5px;
|
||||||
|
position: relative;
|
||||||
|
max-width: 100%;
|
||||||
|
width: fit-content;
|
||||||
|
height: 328px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 220px 600px 80px;
|
||||||
|
grid-template-rows: 55px 55px 190px auto;
|
||||||
|
column-gap: 10px;
|
||||||
|
grid-template-areas:
|
||||||
|
"cover header header"
|
||||||
|
"cover alltags alltags"
|
||||||
|
"cover description description"
|
||||||
|
"cover footer button";
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult p {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult > img {
|
||||||
|
grid-area: cover;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult > .connector-name {
|
||||||
|
grid-area: cover;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
left: 2px;
|
||||||
|
top: 2px;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 2px 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult > .Manga-status {
|
||||||
|
grid-area: header;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult > .Manga-name {
|
||||||
|
grid-area: header;
|
||||||
|
color: black;
|
||||||
|
padding: 0 30px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult > .Manga-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
grid-area: alltags;
|
||||||
|
color: white;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult > .Manga-tags p {
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 10pt;
|
||||||
|
height: fit-content;
|
||||||
|
width: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult .Manga-author {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult .Manga-tag {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult > .Manga-description {
|
||||||
|
grid-area: description;
|
||||||
|
color: black;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult > .Manga-AddButton {
|
||||||
|
grid-area: button;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult > .Manga-AddButton:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult a, .SearchResult a:visited {
|
||||||
|
color: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchResult a img {
|
||||||
|
filter: brightness(0) saturate(100%) invert(0%) sepia(0%) saturate(7480%) hue-rotate(141deg) brightness(111%) contrast(99%);
|
||||||
|
position: relative;
|
||||||
|
bottom: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitorMangaEntry {
|
||||||
|
position: relative;
|
||||||
|
}
|
@ -1,960 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
body{
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: var(--background-color);
|
|
||||||
font-family: "Inter", sans-serif;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
background-placeholder{
|
|
||||||
background-color: var(--second-background-color);
|
|
||||||
opacity: 1;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 0 0 5px 0;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
topbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: var(--topbar-height);
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
z-index: 100;
|
|
||||||
box-shadow: 0 0 20px black;
|
|
||||||
}
|
|
||||||
|
|
||||||
titlebox {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
margin: 0 0 0 40px;
|
|
||||||
height: 100%;
|
|
||||||
align-items:center;
|
|
||||||
justify-content:center;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
titlebox img {
|
|
||||||
height: 100%;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
spacer{
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
filter-box {
|
|
||||||
display: none;
|
|
||||||
align-self: center;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
margin: 10px;
|
|
||||||
background-color: var(--second-background-color);
|
|
||||||
border-style: solid;
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
border-width: 2px;
|
|
||||||
border-radius: 15px;
|
|
||||||
min-width: 300px;
|
|
||||||
width: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
max-height: 50%;
|
|
||||||
height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
filter-box.animate {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
filter-box border-bar popup-title{
|
|
||||||
font-size: 12pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
filter-box border-bar popup-close {
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
font-size: 12pt;
|
|
||||||
-webkit-user-select: none; /* Safari */
|
|
||||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
|
||||||
user-select: none; /* Standard syntax */
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bar-button.clearFilter{
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0px 10px 10px 10px;
|
|
||||||
border-color: lightgray;
|
|
||||||
color: gray;
|
|
||||||
align-content: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bar-button.clearFilter:hover {
|
|
||||||
background-color: red;
|
|
||||||
border-color: var(--second-background-color);
|
|
||||||
color: var(--second-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
status-filter {
|
|
||||||
display: block;
|
|
||||||
margin: 10px;
|
|
||||||
|
|
||||||
/*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;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-user-select: none; /* Safari */
|
|
||||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
|
||||||
user-select: none; /* Standard syntax */
|
|
||||||
}
|
|
||||||
|
|
||||||
status-filter[release-status="Ongoing"]{
|
|
||||||
background-color: limegreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
status-filter[release-status="Completed"]{
|
|
||||||
background-color: blueviolet;
|
|
||||||
}
|
|
||||||
|
|
||||||
status-filter[release-status="On Hiatus"]{
|
|
||||||
background-color: darkorange;
|
|
||||||
}
|
|
||||||
|
|
||||||
status-filter[release-status="Cancelled"]{
|
|
||||||
background-color: firebrick;
|
|
||||||
}
|
|
||||||
|
|
||||||
status-filter[release-status="Upcoming"]{
|
|
||||||
background-color: aqua;
|
|
||||||
}
|
|
||||||
|
|
||||||
status-filter[release-status="Status Unavailable"]{
|
|
||||||
background-color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
searchdiv{
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#searchbox {
|
|
||||||
display: flex;
|
|
||||||
padding: 3px 5px;
|
|
||||||
margin: 5px;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 2px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 12pt;
|
|
||||||
outline: none;
|
|
||||||
border-color: lightgray;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#searchbox:focus {
|
|
||||||
border-color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill {
|
|
||||||
flex-grow: 0;
|
|
||||||
height: 14pt;
|
|
||||||
font-size: 12pt;
|
|
||||||
border-radius: 9pt;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
padding: 2pt 17px;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
#connectorFilterBox .pill {
|
|
||||||
margin: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-user-select: none; /* Safari */
|
|
||||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
|
||||||
user-select: none; /* Standard syntax */
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingscog {
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 0px 30px;
|
|
||||||
margin-left: 15px;
|
|
||||||
height: 50%;
|
|
||||||
filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#filterFunnel {
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 0px 15px;
|
|
||||||
height: 50%;
|
|
||||||
filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%);
|
|
||||||
}
|
|
||||||
|
|
||||||
viewport {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
flex-grow: 1;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: scroll;
|
|
||||||
scrollbar-color: var(--accent-color) var(--primary-color);
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer > div {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0 30px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer > div > *{
|
|
||||||
height: 40%;
|
|
||||||
margin: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#madeWith {
|
|
||||||
flex-grow: 1;
|
|
||||||
text-align: right;
|
|
||||||
margin-right: 20px;
|
|
||||||
cursor: url("media/blahaj.png"), grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
content {
|
|
||||||
position: relative;
|
|
||||||
flex-grow: 1;
|
|
||||||
border-radius: 5px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: start;
|
|
||||||
align-content: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsPopup{
|
|
||||||
z-index: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup{
|
|
||||||
display: none;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 2;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--accent-color);
|
|
||||||
font-weight: bolder;
|
|
||||||
padding: 7px 5px;
|
|
||||||
margin:0;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup-title {
|
|
||||||
font-size: 14pt;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 3px;
|
|
||||||
margin-left: 5px;
|
|
||||||
color: var(--second-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
popup-close {
|
|
||||||
border: none;
|
|
||||||
background-color: inherit;
|
|
||||||
color: var(--second-background-color);;
|
|
||||||
font-weight: inherit;
|
|
||||||
font-size: 27px;
|
|
||||||
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
|
|
||||||
display: flex;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 15px;
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
align-content: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup-close:hover {
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bar > .button-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-right: 0;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bar-button {
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 2px;
|
|
||||||
background-color: inherit;
|
|
||||||
color: var(--second-background-color);
|
|
||||||
font-weight: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
font-family: inherit;
|
|
||||||
display: flex;
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 0px 5px;
|
|
||||||
padding: 5px 20px;
|
|
||||||
border-radius: 20px;
|
|
||||||
height: 20px;
|
|
||||||
align-items: center;
|
|
||||||
border-color: var(--accent-color);
|
|
||||||
-webkit-user-select: none; /* Safari */
|
|
||||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
|
||||||
user-select: none; /* Standard syntax */
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bar-button:hover {
|
|
||||||
border-color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bar-button.primary {
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
color: var(--accent-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bar-button.primary:hover {
|
|
||||||
border-color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bar-button.section {
|
|
||||||
font-weight: bold;
|
|
||||||
color: darkgray;
|
|
||||||
border-color: darkgray;
|
|
||||||
text-align: center;
|
|
||||||
padding: 5px;
|
|
||||||
flex-grow: 1;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bar-button.section:hover {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
border-color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
popup popup-window {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 3;
|
|
||||||
left: 10%;
|
|
||||||
top: 10%;
|
|
||||||
height: 80%;
|
|
||||||
width: 80%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--second-background-color);
|
|
||||||
border-radius: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup#jobStatusView popup-window {
|
|
||||||
left: 20%;
|
|
||||||
top: 20%;
|
|
||||||
height: 60%;
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup-content{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: left;
|
|
||||||
height: calc(100% - 60px);
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--secondary-color) var(--second-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
popup-content > .popup-section {
|
|
||||||
margin: 5px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 10pt;
|
|
||||||
font-weight: 100;
|
|
||||||
display: block;
|
|
||||||
border-top-style: solid;
|
|
||||||
border-top-width: 1px;
|
|
||||||
border-top-color: lightgray;
|
|
||||||
width: calc(100%-10px);
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 22%;
|
|
||||||
min-width: 300px;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 10px;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: lightgray;
|
|
||||||
margin: 7px;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item.dyn-height {
|
|
||||||
height: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .title {
|
|
||||||
font-weight: bold;
|
|
||||||
vertical-align: bottom;
|
|
||||||
line-height: 32px;
|
|
||||||
font-size: 12pt;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:link {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:visited {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: underline solid var(--secondary-color) 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:active {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .title > img {
|
|
||||||
width: auto;
|
|
||||||
height: 32px;
|
|
||||||
margin: 5px;
|
|
||||||
vertical-align: middle;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .title > connector-configured {
|
|
||||||
display:block;
|
|
||||||
height: 10px;
|
|
||||||
width: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin: 5px;
|
|
||||||
float: right;
|
|
||||||
top: 5px;
|
|
||||||
right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .title > connector-configured::after {
|
|
||||||
display: block;
|
|
||||||
content: attr(configuration);
|
|
||||||
float: right;
|
|
||||||
width: max-content;
|
|
||||||
width: -webkit-max-content;
|
|
||||||
width: -mox-max-content;
|
|
||||||
width: intrinsic;
|
|
||||||
|
|
||||||
visibility: hidden;
|
|
||||||
|
|
||||||
/*Text Properties*/
|
|
||||||
font-size:8pt;
|
|
||||||
font-weight:bold;
|
|
||||||
color:white;
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
/*Size*/
|
|
||||||
padding: 0px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 0px;
|
|
||||||
background-color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .title > connector-configured:hover::after{
|
|
||||||
visibility:visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .title > connector-configured[configuration="Active"] {
|
|
||||||
background-color: limegreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .title > connector-configured[configuration="Not Configured"] {
|
|
||||||
background-color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > input {
|
|
||||||
margin: 2px;
|
|
||||||
padding: 5px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border-style: solid;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.section-item > input:focus {
|
|
||||||
border-color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > row {
|
|
||||||
width: calc(100%-20px);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > row > input {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 2px;
|
|
||||||
padding: 5px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border-style: solid;
|
|
||||||
outline: none;
|
|
||||||
flex-grow: 0;
|
|
||||||
text-align: end;
|
|
||||||
float: right;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
.section-item > row > input:focus {
|
|
||||||
border-color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > row > select {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 2px;
|
|
||||||
padding: 2px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border-style: solid;
|
|
||||||
outline: none;
|
|
||||||
flex-grow: 0;
|
|
||||||
text-align: end;
|
|
||||||
float: right;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > row > select:focus {
|
|
||||||
border-color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-buttons-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-buttons-container > .section-button {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 3px 10px;
|
|
||||||
margin: 3px;
|
|
||||||
border-radius: 5px;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: lightgray;
|
|
||||||
font-weight: bold;
|
|
||||||
color: gray;
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-user-select: none; /* Safari */
|
|
||||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
|
||||||
user-select: none; /* Standard syntax */
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-button#reset:hover {
|
|
||||||
color: red;
|
|
||||||
border-color: red;
|
|
||||||
}
|
|
||||||
.section-buttons-container > .section-button:hover {
|
|
||||||
border-color: var(--secondary-color);
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#newMangaPopup > div {
|
|
||||||
z-index: 3;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#newMangaPopup > #newMangaPopupSelector {
|
|
||||||
width: 600px;
|
|
||||||
height: 40px;
|
|
||||||
margin: 80px auto 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#newMangaPopup > div > #newMangaConnector, #newMangaTitle, #newMangaTranslatedLanguage {
|
|
||||||
margin: 0;
|
|
||||||
display: inline-block;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#newMangaPopup #newMangaConnector {
|
|
||||||
width: 100px;
|
|
||||||
padding: 0 0 0 5px;
|
|
||||||
border-radius: 5px 0 0 5px;
|
|
||||||
border: 0;
|
|
||||||
border-right: 1px solid darkgray;
|
|
||||||
}
|
|
||||||
|
|
||||||
#newMangaPopup #newMangaTitle{
|
|
||||||
width: 445px;
|
|
||||||
padding: 0 5px 0 5px;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#newMangaPopup #newMangaTranslatedLanguage {
|
|
||||||
width: 45px;
|
|
||||||
border-radius: 0 5px 5px 0;
|
|
||||||
border: 0;
|
|
||||||
border-left: 1px solid darkgray;
|
|
||||||
margin-left: -5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#newMangaResult {
|
|
||||||
display: none;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin: 5px auto 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 5px;
|
|
||||||
width: min-content;
|
|
||||||
max-width: 98%;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-x: scroll;
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
blur-background {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
background: var(--blur-background);
|
|
||||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
|
||||||
backdrop-filter: blur(4.5px);
|
|
||||||
-webkit-backdrop-filter: blur(4.5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#publicationViewerPopup{
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer{
|
|
||||||
display: block;
|
|
||||||
width: 460px;
|
|
||||||
position: absolute;
|
|
||||||
top: 200px;
|
|
||||||
left: 400px;
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer::after{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0; top: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0,0,0,0.8);
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer img {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 5px;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details > * {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details publication-name {
|
|
||||||
width: initial;
|
|
||||||
overflow-x: scroll;
|
|
||||||
white-space: nowrap;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details publication-tags::before {
|
|
||||||
content: "Tags";
|
|
||||||
display: block;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details publication-tags {
|
|
||||||
overflow-x: scroll;
|
|
||||||
white-space: nowrap;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details publication-author::before {
|
|
||||||
content: "Author: ";
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details publication-description::before {
|
|
||||||
content: "Description";
|
|
||||||
display: block;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details publication-description {
|
|
||||||
font-size: 12pt;
|
|
||||||
margin: 5px 0;
|
|
||||||
height: 145px;
|
|
||||||
overflow-x: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details publication-interactions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: end;
|
|
||||||
align-items: start;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details publication-interactions > * {
|
|
||||||
margin: 0 10px;
|
|
||||||
font-size: 16pt;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details publication-interactions publication-starttask {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details publication-interactions publication-delete {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-view publication-details publication-interactions publication-canceltask {
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-viewer publication-details publication-interactions publication-add {
|
|
||||||
color: limegreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer-tag-popup {
|
|
||||||
display: none;
|
|
||||||
padding: 2px 4px;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 58px;
|
|
||||||
left: 20px;
|
|
||||||
background-color: var(--second-background-color);
|
|
||||||
z-index: 8;
|
|
||||||
border-radius: 5px;
|
|
||||||
max-height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer-tag-content{
|
|
||||||
position: relative;
|
|
||||||
max-height: 400px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer-tag-content > * {
|
|
||||||
margin: 2px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer-tag-popup::before{
|
|
||||||
content: "";
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
position: absolute;
|
|
||||||
border-right: 10px solid var(--second-background-color);
|
|
||||||
border-left: 10px solid transparent;
|
|
||||||
border-top: 10px solid var(--second-background-color);
|
|
||||||
border-bottom: 10px solid transparent;
|
|
||||||
left: 0;
|
|
||||||
bottom: -17px;
|
|
||||||
border-radius: 0 0 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loaderdiv {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader {
|
|
||||||
border: 16px solid transparent;
|
|
||||||
border-top: 16px solid var(--secondary-color);
|
|
||||||
border-bottom: 16px solid var(--primary-color);
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
animation: spin 2s linear infinite;
|
|
||||||
position: absolute;
|
|
||||||
left: calc(50% - 60px);
|
|
||||||
top: calc(50% - 120px);
|
|
||||||
z-index: 201;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loaderText {
|
|
||||||
position: relative;
|
|
||||||
margin: 0 auto;
|
|
||||||
top: calc(50% + 80px);
|
|
||||||
z-index: 201;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--second-background-color);
|
|
||||||
font-size: 20pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
#jobStatusRunning > .section-item {
|
|
||||||
flex-direction: row;
|
|
||||||
height: 150px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#jobStatusWaiting > .section-item {
|
|
||||||
flex-direction: row;
|
|
||||||
height: 150px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .jobImage {
|
|
||||||
height: 100%;
|
|
||||||
width: auto;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobDetails {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .jobDetails > .jobTitle {
|
|
||||||
margin: 5px;
|
|
||||||
font-size: 11pt;
|
|
||||||
font-weight: bold;
|
|
||||||
text-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .jobDetails > .jobProgressBar {
|
|
||||||
margin: 5px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .jobDetails > .jobProgressSpan {
|
|
||||||
margin: 5px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item > .jobDetails > .jobCancel {
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 5px;
|
|
||||||
font-size: 12pt;
|
|
||||||
color: var(--secondary-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
@ -1,172 +0,0 @@
|
|||||||
#addPublication {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
width: 180px;
|
|
||||||
height: 300px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 10px 10px;
|
|
||||||
padding: 15px 20px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#addPublication p{
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 150pt;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 300px;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill {
|
|
||||||
flex-grow: 0;
|
|
||||||
height: 14pt;
|
|
||||||
font-size: 12pt;
|
|
||||||
border-radius: 9pt;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
padding: 2pt 17px;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication{
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
width: 180px;
|
|
||||||
height: 300px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 10px 10px;
|
|
||||||
padding: 15px 19px;
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication:hover {
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication:hover::after{
|
|
||||||
background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
|
|
||||||
}
|
|
||||||
|
|
||||||
publication:hover > publication-information {
|
|
||||||
display: flex;
|
|
||||||
opacity:1;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication::after{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0; top: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 100%; height: 100%;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-information {
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-information * {
|
|
||||||
z-index: 1;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
connector-name{
|
|
||||||
width: fit-content;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-name{
|
|
||||||
width: fit-content;
|
|
||||||
font-size: 16pt;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status {
|
|
||||||
display:block;
|
|
||||||
height: 10px;
|
|
||||||
width: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin: 5px;
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
right: 5px;
|
|
||||||
z-index: 2;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 10px, rgb(51, 51, 51) 0px 0px 10px 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status::after {
|
|
||||||
content: attr(release-status);
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status:hover::after{
|
|
||||||
visibility:visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
publication-status[release-status="Ongoing"]{
|
|
||||||
background-color: limegreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status[release-status="Completed"]{
|
|
||||||
background-color: blueviolet;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status[release-status="On Hiatus"]{
|
|
||||||
background-color: darkorange;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status[release-status="Cancelled"]{
|
|
||||||
background-color: firebrick;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status[release-status="Upcoming"]{
|
|
||||||
background-color: aqua;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status[release-status="Status Unavailable"]{
|
|
||||||
background-color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
publication-details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-details * {
|
|
||||||
z-index: 1;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
publication img {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
z-index: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
42
Website/styles/footer.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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("Website/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;
|
||||||
|
}
|
33
Website/styles/header.css
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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;
|
||||||
|
}
|
42
Website/styles/index.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
: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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
39
Website/styles/monitorMangaList.css
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#MonitorMangaList {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
scrollbar-color: var(--accent-color) var(--primary-color);
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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%);
|
||||||
|
}
|
43
Website/styles/popup.css
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.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: 100%;
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
}
|
81
Website/styles/queuePopUp.css
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#QueuePopUp #QueuePopUpBody {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
143
Website/styles/react-toggle.css
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
/* https://raw.githubusercontent.com/instructure-react/react-toggle/master/style.css */
|
||||||
|
|
||||||
|
.react-toggle {
|
||||||
|
touch-action: pan-x;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle-screenreader-only {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle--disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
-webkit-transition: opacity 0.25s;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle-track {
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 30px;
|
||||||
|
background-color: #4D4D4D;
|
||||||
|
-webkit-transition: all 0.2s ease;
|
||||||
|
-moz-transition: all 0.2s ease;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle--checked .react-toggle-track {
|
||||||
|
background-color: #19AB27;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
|
||||||
|
background-color: #128D15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle-track-check {
|
||||||
|
position: absolute;
|
||||||
|
width: 14px;
|
||||||
|
height: 10px;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
line-height: 0;
|
||||||
|
left: 8px;
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.25s ease;
|
||||||
|
-moz-transition: opacity 0.25s ease;
|
||||||
|
transition: opacity 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle--checked .react-toggle-track-check {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transition: opacity 0.25s ease;
|
||||||
|
-moz-transition: opacity 0.25s ease;
|
||||||
|
transition: opacity 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle-track-x {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
line-height: 0;
|
||||||
|
right: 10px;
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transition: opacity 0.25s ease;
|
||||||
|
-moz-transition: opacity 0.25s ease;
|
||||||
|
transition: opacity 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle--checked .react-toggle-track-x {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle-thumb {
|
||||||
|
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 1px;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 1px solid #4D4D4D;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #FAFAFA;
|
||||||
|
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
-webkit-transition: all 0.25s ease;
|
||||||
|
-moz-transition: all 0.25s ease;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle--checked .react-toggle-thumb {
|
||||||
|
left: 27px;
|
||||||
|
border-color: #19AB27;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle--focus .react-toggle-thumb {
|
||||||
|
-webkit-box-shadow: 0px 0px 3px 2px #0099E0;
|
||||||
|
-moz-box-shadow: 0px 0px 3px 2px #0099E0;
|
||||||
|
box-shadow: 0px 0px 2px 3px #0099E0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-toggle:active:not(.react-toggle--disabled) .react-toggle-thumb {
|
||||||
|
-webkit-box-shadow: 0px 0px 5px 5px #0099E0;
|
||||||
|
-moz-box-shadow: 0px 0px 5px 5px #0099E0;
|
||||||
|
box-shadow: 0px 0px 5px 5px #0099E0;
|
||||||
|
}
|
58
Website/styles/search.css
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#Search{
|
||||||
|
position: relative;
|
||||||
|
width: 98vw;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#SearchBox{
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#SearchResults {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#SearchBox select, #SearchBox button, #SearchBox input {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-top-width: 2px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#Searchbox-Manganame {
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-left-width: 2px !important;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 50vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
#Searchbox-connector {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
#Searchbox-language {
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#Searchbox-button {
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
border-right-width: 2px !important;
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#closeSearch {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
filter: brightness(0) saturate(100%) invert(100%) sepia(100%) saturate(1%) hue-rotate(20deg) brightness(103%) contrast(101%);
|
||||||
|
}
|
101
Website/styles/settings.css
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
#Settings {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#SettingsIcon {
|
||||||
|
height: calc(100% - 26px);
|
||||||
|
margin: 13px;
|
||||||
|
filter: invert(100%) sepia(65%) saturate(2%) hue-rotate(215deg) brightness(113%) contrast(101%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#settingsPopupBody {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
margin: 5px 5px 10px;
|
||||||
|
font-size: 10pt;
|
||||||
|
font-weight: 100;
|
||||||
|
display: block;
|
||||||
|
border-top-style: solid;
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-top-color: lightgray;
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 22%;
|
||||||
|
min-width: 300px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: lightgray;
|
||||||
|
margin: 7px;
|
||||||
|
padding: 5px 5px 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-item > * {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-item[connector-status="Not Configured"]{
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-item[connector-status="Configured"]{
|
||||||
|
border-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-item > .settings-section-title {
|
||||||
|
font-weight: bold;
|
||||||
|
vertical-align: bottom;
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: 12pt;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-item > .settings-section-title > img {
|
||||||
|
width: auto;
|
||||||
|
height: 32px;
|
||||||
|
margin: 5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-item .section-actions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin: 5px;
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-actions > span, #resetUserAgent {
|
||||||
|
border: 1px solid lightgray;
|
||||||
|
padding: 3px 5px 2px;
|
||||||
|
width: 10ch;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resetUserAgent{
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
3213
package-lock.json
generated
Normal file
23
package.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@mdi/js": "^7.4.47",
|
||||||
|
"@mdi/react": "^1.6.1",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-toggle": "^4.0.5",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-cookie": "^7.2.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-toggle": "^4.1.3",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"vite": "^5.4.9"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ramonak/react-progress-bar": "^5.3.0",
|
||||||
|
"@uiw/react-markdown-preview": "^5.1.3"
|
||||||
|
}
|
||||||
|
}
|
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"lib": [
|
||||||
|
"es2020",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
}
|
||||||
|
}
|
9
vite.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// vite.config.js
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: '8080'
|
||||||
|
},
|
||||||
|
root: 'Website'
|
||||||
|
})
|