Compare commits
203 Commits
master
...
121b9ed7f6
Author | SHA1 | Date | |
---|---|---|---|
121b9ed7f6 | |||
34a34ef91e | |||
6ee200e482 | |||
1e2ef1d90c | |||
b98bd3a11d | |||
ee1b0536c7 | |||
d5975192e8 | |||
9e388c8ac8 | |||
87589a2a11 | |||
7e610c485b | |||
34dcd06ca9 | |||
5f74743d8c | |||
257a76b42a | |||
0dd8ad9164 | |||
74feb160c0 | |||
c3eb42449f | |||
39f6cbdbfd | |||
8d6ef2388c | |||
476762a30f | |||
cd566f01e1 | |||
7b038ad377 | |||
30ed3d8a2d | |||
f19390f52d | |||
b8350602c5 | |||
b4e25a9f54 | |||
5c78896a5d | |||
e253ae3d20 | |||
84f9dc176e | |||
4c81c571a4 | |||
b4b4479e9b | |||
d6eeae4021 | |||
6b05c4fb00 | |||
d785b6e078 | |||
e64f325022 | |||
486f6f60fb | |||
01c732f6a8 | |||
e5641e0690 | |||
b7ee80b4a4 | |||
9f8d874822 | |||
a273af5ed9 | |||
be704d922a | |||
692bf3561b | |||
e0ec64882b | |||
496e6fdde8 | |||
645d3c8793 | |||
e1b590482c | |||
94e58d2d1b | |||
d5c85efcb2 | |||
aed0a562f0 | |||
ddaa066dc7 | |||
a6d2de35cf | |||
7cdc31fedb | |||
dcf596df84 | |||
855fe91e69 | |||
c00c80503c | |||
4e648bd04e | |||
35e2e6ce16 | |||
dbdb3c0168 | |||
dae653c2b4 | |||
e9a2a561cf | |||
5fe3ead54d | |||
6d8d0ec636 | |||
724dc9b986 | |||
a59920f09d | |||
5c5f810127 | |||
181d591344 | |||
0193077377 | |||
9fa57eacdb | |||
c4cc21f866 | |||
f391ace9b2 | |||
a0d15e08a7 | |||
11549a07c3 | |||
79c13ceb1d | |||
0907031583 | |||
00932bf6bd | |||
bbad467fe6 | |||
03c78f7b6a | |||
3ad7fbfad2 | |||
6173d16edc | |||
8a07818b89 | |||
6aa53f7ca7 | |||
415e6c606f | |||
178e8b72f3 | |||
65457c289f | |||
6109bb678a | |||
cc14c5adad | |||
8c0f61fc6a | |||
82389c4847 | |||
a91452ee8c | |||
698e16cc2b | |||
4e20b95d77 | |||
5626a9d0fd | |||
10f48af9fa | |||
5e95c34306 | |||
66dcb2f1e6 | |||
19e033995d | |||
99265bacb2 | |||
5385dfd918 | |||
d6e4d1d27f | |||
288cd77049 | |||
e605578a34 | |||
6340c5ad03 | |||
6d402357e7 | |||
9e0eb0262e | |||
63b220bd79 | |||
d480f62e51 | |||
44755675e5 | |||
6b10aa8926 | |||
48b669dc07 | |||
fbb25807e6 | |||
44f43b7152 | |||
68653778c8 | |||
fd4c01203f | |||
d358611133 | |||
b0cbb416d6 | |||
e3ad70c0a9 | |||
f38290b2ed | |||
211db3d4d5 | |||
a3046680ac | |||
1ed376ba47 | |||
63b10600da | |||
5333c025f7 | |||
187dd22027 | |||
2092db2ba3 | |||
ecd76712d8 | |||
60f957ede2 | |||
fcd232bec9 | |||
357fca3bd5 | |||
1d8dd7381d | |||
0b0abb3801 | |||
62665f5660 | |||
007b49c624 | |||
d9bbbed1c0 | |||
42a1e1a2ce | |||
aad18c0195 | |||
1eef710efc | |||
84520d8e18 | |||
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 |
@@ -38,7 +38,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
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/
|
||||||
|
17
Dockerfile
@@ -1,6 +1,19 @@
|
|||||||
FROM nginx:alpine3.17-slim
|
# Build stage
|
||||||
COPY ./Website /usr/share/nginx/html
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY ./tranga-website /app
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Serve stage
|
||||||
|
FROM nginx:alpine3.17-slim
|
||||||
|
|
||||||
|
# Copy built files from Vite's dist folder
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
#COPY --from=builder /app/tranga-website/media /usr/share/nginx/html/media
|
||||||
COPY ./nginx /etc/nginx
|
COPY ./nginx /etc/nginx
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
ENV API_URL=http://tranga-api:6531
|
ENV API_URL=http://tranga-api:6531
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
100
README.md
@@ -1,11 +1,6 @@
|
|||||||
# Testers for V2 wanted!
|
|
||||||
|
|
||||||
[Details](https://github.com/C9Glax/tranga/pull/355#issuecomment-2764217944)
|
|
||||||
|
|
||||||
<!-- PROJECT LOGO -->
|
|
||||||
<br />
|
<br />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
<span id="readme-top"></span>
|
||||||
<h3 align="center">Tranga-Website</h3>
|
<h3 align="center">Tranga-Website</h3>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -14,45 +9,48 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
This is the Website for <a href="https://github.com/C9Glax/tranga">Tranga</a> (API)
|
This is the Website for <a href="https://github.com/C9Glax/tranga">Tranga</a> (API)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga-website/master?label=master"></th>
|
||||||
|
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga-website%2Factions%2Fworkflows%2Fdocker-image-master.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga-website/cuttingedge?label=cuttingedge"></th>
|
||||||
|
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga-website%2Factions%2Fworkflows%2Fdocker-image-cuttingedge.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga-website/vite-react-ts?label=vite-react-ts"></th>
|
||||||
|
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga-website%2Factions%2Fworkflows%2Fdocker-image-vite-react-ts.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- TABLE OF CONTENTS -->
|
|
||||||
<details>
|
|
||||||
<summary>Table of Contents</summary>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<a href="#about-the-project">About The Project</a>
|
|
||||||
<ul>
|
|
||||||
<li><a href="#built-with">Built With</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#getting-started">Getting Started</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="#roadmap">Roadmap</a></li>
|
|
||||||
<li><a href="#contributing">Contributing</a></li>
|
|
||||||
<li><a href="#license">License</a></li>
|
|
||||||
<li><a href="#acknowledgments">Acknowledgments</a></li>
|
|
||||||
</ol>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ABOUT THE PROJECT -->
|
<!-- ABOUT THE PROJECT -->
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
|  |  |  |
|
||||||
|
|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
|
||||||
|
|  |  |  |
|
||||||
|
|
||||||
## About The Project
|
## About The Project
|
||||||
|
|
||||||
Tranga-Website is the Web-frontend to [Tranga](https://github.com/C9Glax/tranga) (the API). It displays information aquired from Tranga and can create Jobs (Manga-Downloads).
|
Tranga-Website is the Web-frontend to [Tranga](https://github.com/C9Glax/tranga) (the API).
|
||||||
|
|
||||||
### What this does do (and nothing else)
|
### What this does do (and nothing else)
|
||||||
|
|
||||||
This repo makes HTTP-requests to the [Tranga-API](https://github.com/C9Glax/tranga) to display it's present configuration.
|
This project makes HTTP-requests to the [Tranga-API](https://github.com/C9Glax/tranga) to display and modify the present configuration.
|
||||||
|
|
||||||
### 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>
|
||||||
@@ -60,49 +58,17 @@ This repo makes HTTP-requests to the [Tranga-API](https://github.com/C9Glax/tran
|
|||||||
<!-- GETTING STARTED -->
|
<!-- GETTING STARTED -->
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
There is a single release:
|
Go to [Tranga](https://github.com/C9Glax/tranga?tab=readme-ov-file#getting-started) and read the README there.
|
||||||
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
<!-- 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
|
||||||
|
|
||||||
The following is copy & pasted:
|
Go to [Tranga](https://github.com/C9Glax/tranga?tab=readme-ov-file#contributing) and read the README there.
|
||||||
|
|
||||||
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
|
||||||
|
|
||||||
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
|
|
||||||
Don't forget to give the project a star! Thanks again!
|
|
||||||
|
|
||||||
1. Fork the Project
|
|
||||||
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
|
|
||||||
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
|
|
||||||
4. Push to the Branch (`git push origin feature/AmazingFeature`)
|
|
||||||
5. Open a Pull Request
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- LICENSE -->
|
<!-- LICENSE -->
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the GNU GPLv3 License. See `LICENSE.txt` for more information.
|
See `LICENSE.txt` for more information.
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
|
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 |
BIN
Screenshots/Screenshot 2025-06-17 at 01-15-08 Tranga.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
Screenshots/Screenshot 2025-06-17 at 01-15-23 Tranga.png
Normal file
After Width: | Height: | Size: 522 KiB |
BIN
Screenshots/Screenshot 2025-06-17 at 01-15-36 Tranga.png
Normal file
After Width: | Height: | Size: 876 KiB |
BIN
Screenshots/Screenshot 2025-06-17 at 01-16-32 Tranga.png
Normal file
After Width: | Height: | Size: 890 KiB |
BIN
Screenshots/Screenshot 2025-06-17 at 01-16-43 Tranga.png
Normal file
After Width: | Height: | Size: 718 KiB |
BIN
Screenshots/Screenshot 2025-06-17 at 01-16-54 Tranga.png
Normal file
After Width: | Height: | Size: 456 KiB |
BIN
Screenshots/Screenshot 2025-06-17 at 01-17-16 Tranga.png
Normal file
After Width: | Height: | Size: 555 KiB |
BIN
Screenshots/Screenshot 2025-06-17 at 01-17-37 Tranga.png
Normal file
After Width: | Height: | Size: 544 KiB |
@@ -1,344 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 440 B |
@@ -1,113 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
height="512pt"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
width="512pt"
|
|
||||||
version="1.1"
|
|
||||||
id="svg4586"
|
|
||||||
sodipodi:docname="komga - Copy.svg"
|
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
|
||||||
<metadata
|
|
||||||
id="metadata4592">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs4590">
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient6082"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#000000;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop6080"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient6076"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#000000;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop6074"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient6082"
|
|
||||||
id="linearGradient6084"
|
|
||||||
x1="77.866812"
|
|
||||||
y1="386.00679"
|
|
||||||
x2="217.20259"
|
|
||||||
y2="386.00679"
|
|
||||||
gradientUnits="userSpaceOnUse"/>
|
|
||||||
</defs>
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1656"
|
|
||||||
inkscape:window-height="1368"
|
|
||||||
id="namedview4588"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="1.2512475"
|
|
||||||
inkscape:cx="264.73114"
|
|
||||||
inkscape:cy="305.20589"
|
|
||||||
inkscape:window-x="-7"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg4586"/>
|
|
||||||
<path
|
|
||||||
d="m512 256c0 141.386719-114.613281 256-256 256s-256-114.613281-256-256 114.613281-256 256-256 256 114.613281 256 256zm0 0"
|
|
||||||
fill="#005ed3"
|
|
||||||
id="path4556"/>
|
|
||||||
<path
|
|
||||||
d="m 512,256 c 0,-11.71094 -0.80469,-23.23047 -2.32422,-34.52344 L 382.48047,94.28125 320.52344,121.85938 256,56.933594 212.69531,131.30469 129.51953,94.28125 141.86719,178.42187 49.949219,193.81641 114.32031,256 l -64.371091,62.18359 82.121091,82.16016 -2.55078,17.375 91.95703,91.95703 C 232.76953,511.19531 244.28906,512 256,512 397.38672,512 512,397.38672 512,256 Z"
|
|
||||||
id="path4558"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
style="fill:#00459f"
|
|
||||||
sodipodi:nodetypes="scccccccccccccss"/>
|
|
||||||
<path
|
|
||||||
d="m256 86.742188 37.109375 63.738281 70.574219-31.414063-10.527344 71.71875 77.078125 12.910156-54.144531 52.304688 54.144531 52.304688-77.078125 12.910156 10.527344 71.71875-70.574219-31.414063-37.109375 63.738281-37.109375-63.738281-70.574219 31.414063 10.527344-71.71875-77.078125-12.910156 54.144531-52.304688-54.144531-52.304688 77.078125-12.910156-10.527344-71.71875 70.574219 31.414063zm0 0"
|
|
||||||
fill="#ff0335"
|
|
||||||
id="path4560"/>
|
|
||||||
<path
|
|
||||||
d="m430.230469 308.300781-77.070313 12.910157 10.519532 71.71875-70.570313-31.410157-37.109375 63.742188v-338.523438l37.109375 63.742188 70.570313-31.410157-6.757813 46.101563-3.761719 25.617187 58.800782 9.851563 18.269531 3.058594-13.390625 12.929687-40.75 39.371094 11.378906 10.988281zm0 0"
|
|
||||||
fill="#c2001b"
|
|
||||||
id="path4562"/>
|
|
||||||
<path
|
|
||||||
d="m256 455.066406-43.304688-74.371094-83.175781 37.023438 12.347657-84.140625-91.917969-15.394531 64.371093-62.183594-64.371093-62.183594 91.917969-15.394531-12.347657-84.140625 83.179688 37.023438 43.300781-74.371094 43.304688 74.371094 83.175781-37.023438-12.347657 84.140625 91.917969 15.394531-64.371093 62.183594 64.371093 62.183594-91.917969 15.398437 12.347657 84.136719-83.175781-37.023438zm-30.917969-112.722656 30.917969 53.101562 30.917969-53.101562 57.964843 25.800781-8.703124-59.292969 62.238281-10.425781-43.917969-42.425781 43.917969-42.425781-62.238281-10.425781 8.703124-59.292969-57.964843 25.800781-30.917969-53.101562-30.917969 53.101562-57.964843-25.800781 8.703124 59.292969-62.238281 10.425781 43.917969 42.425781-43.917969 42.425781 62.238281 10.425781-8.703124 59.292969zm0 0"
|
|
||||||
fill="#ffdf47"
|
|
||||||
id="path4564"/>
|
|
||||||
<path
|
|
||||||
d="m403.308594 261.441406-5.628906-5.441406 25.160156-24.300781 39.210937-37.878907-55.75-9.339843-36.171875-6.058594 2.800782-19.09375 9.550781-65.046875-83.179688 37.019531-43.300781-74.371093v59.621093l30.921875 53.109375 57.957031-25.808594-3.910156 26.667969-2.546875 17.378907-2.242187 15.25 2.480468.421874 59.761719 10.007813-43.921875 42.421875 16.96875 16.390625 26.953125 26.03125-62.242187 10.429687 8.699218 59.296876-57.957031-25.808594-30.921875 53.109375v59.621093l43.300781-74.371093 83.179688 37.019531-12.351563-84.140625 91.921875-15.398437zm0 0"
|
|
||||||
fill="#fec000"
|
|
||||||
id="path4566"/>
|
|
||||||
<g
|
|
||||||
aria-label="K"
|
|
||||||
transform="matrix(1.1590846,-0.34467221,0.22789693,0.794981,0,0)"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:296.55969238px;line-height:125%;font-family:Impact;-inkscape-font-specification:Impact;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="text4596">
|
|
||||||
<path
|
|
||||||
d="m 220.91497,266.9035 -34.89789,105.85211 38.2284,128.58643 H 161.2555 L 136.63873,400.84769 V 501.34204 H 75.676021 V 266.9035 h 60.962709 v 91.08205 l 27.07845,-91.08205 z"
|
|
||||||
style="font-size:296.55969238px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="path824"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 477 B |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.1 KiB |
@@ -1,40 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="50mm" height="50mm" viewBox="0 0 50 50">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="b">
|
|
||||||
<stop offset="0" style="stop-color:#348878;stop-opacity:1"/>
|
|
||||||
<stop offset="1" style="stop-color:#52bca6;stop-opacity:1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="a">
|
|
||||||
<stop offset="0" style="stop-color:#348878;stop-opacity:1"/>
|
|
||||||
<stop offset="1" style="stop-color:#56bda8;stop-opacity:1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient xlink:href="#a" id="e" x1="160.722" x2="168.412" y1="128.533" y2="134.326" gradientTransform="matrix(3.74959 0 0 3.74959 -541.79 -387.599)" gradientUnits="userSpaceOnUse"/>
|
|
||||||
<linearGradient xlink:href="#b" id="c" x1=".034" x2="50.319" y1="0" y2="50.285" gradientTransform="matrix(.99434 0 0 .99434 -.034 0)" gradientUnits="userSpaceOnUse"/>
|
|
||||||
<filter id="d" width="1.176" height="1.211" x="-.076" y="-.092" style="color-interpolation-filters:sRGB">
|
|
||||||
<feFlood flood-color="#fff" flood-opacity=".192" result="flood"/>
|
|
||||||
<feComposite in="flood" in2="SourceGraphic" operator="in" result="composite1"/>
|
|
||||||
<feGaussianBlur in="composite1" result="blur" stdDeviation="4"/>
|
|
||||||
<feOffset dx="3" dy="2.954" result="offset"/>
|
|
||||||
<feComposite in="SourceGraphic" in2="offset" result="composite2"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
<g style="display:inline">
|
|
||||||
<path d="M0 0h50v50H0z" style="fill:url(#c);fill-opacity:1;stroke:none;stroke-width:.286502;stroke-linejoin:bevel"/>
|
|
||||||
</g>
|
|
||||||
<g style="display:inline">
|
|
||||||
<path d="M50.4 46.883c-9.168 0-17.023 7.214-17.023 16.387v.007l.09 71.37-2.303 16.992 31.313-8.319h77.841c9.17 0 17.024-7.224 17.024-16.396V63.27c0-9.17-7.85-16.383-17.016-16.387h-.008zm0 11.566h89.926c3.222.004 5.45 2.347 5.45 4.82v63.655c0 2.475-2.232 4.82-5.457 4.82h-79.54l-15.908 4.807.162-.938-.088-72.343c0-2.476 2.23-4.82 5.455-4.82z" style="color:#fff;display:inline;fill:#fff;stroke:none;stroke-width:1.93113;-inkscape-stroke:none;filter:url(#d)" transform="scale(.26458)"/>
|
|
||||||
</g>
|
|
||||||
<g style="display:inline">
|
|
||||||
<path d="M88.2 95.309H64.92c-1.601 0-2.91 1.236-2.91 2.746l.022 18.602-.435 2.506 6.231-1.881H88.2c1.6 0 2.91-1.236 2.91-2.747v-16.48c0-1.51-1.31-2.746-2.91-2.746z" style="color:#fff;fill:url(#e);stroke:none;stroke-width:2.49558;-inkscape-stroke:none" transform="translate(-51.147 -81.516)"/>
|
|
||||||
<path d="M50.4 46.883c-9.168 0-17.023 7.214-17.023 16.387v.007l.09 71.37-2.303 16.992 31.313-8.319h77.841c9.17 0 17.024-7.224 17.024-16.396V63.27c0-9.17-7.85-16.383-17.016-16.387h-.008zm0 11.566h89.926c3.222.004 5.45 2.347 5.45 4.82v63.655c0 2.475-2.232 4.82-5.457 4.82h-79.54l-15.908 4.807.162-.938-.088-72.343c0-2.476 2.23-4.82 5.455-4.82z" style="color:#fff;fill:#fff;stroke:none;stroke-width:1.93113;-inkscape-stroke:none" transform="scale(.26458)"/>
|
|
||||||
<g style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0;word-spacing:0;fill:#fff;stroke:none;stroke-width:.525121">
|
|
||||||
<path d="M62.57 116.77v-1.312l3.28-1.459q.159-.068.306-.102.158-.045.283-.068l.271-.022v-.09q-.136-.012-.271-.046-.125-.023-.283-.057-.147-.045-.306-.113l-3.28-1.459v-1.323l5.068 2.319v1.413z" style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.45366 0 0 1.72815 -75.122 -171.953)"/>
|
|
||||||
<path d="M62.309 110.31v1.903l3.437 1.53.022.007-.022.008-3.437 1.53v1.892l.37-.17 5.221-2.39v-1.75zm.525.817 4.541 2.08v1.076l-4.541 2.078v-.732l3.12-1.389.003-.002a1.56 1.56 0 0 1 .258-.086h.006l.008-.002c.094-.027.176-.047.246-.06l.498-.041v-.574l-.24-.02a1.411 1.411 0 0 1-.231-.04l-.008-.001-.008-.002a9.077 9.077 0 0 1-.263-.053 2.781 2.781 0 0 1-.266-.097l-.004-.002-3.119-1.39z"
|
|
||||||
style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.45366 0 0 1.72815 -75.122 -171.953)"/>
|
|
||||||
</g>
|
|
||||||
<g style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0;word-spacing:0;fill:#fff;stroke:none;stroke-width:.525121">
|
|
||||||
<path d="M69.171 117.754h5.43v1.278h-5.43Z" style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.44935 0 0 1.66414 -74.104 -166.906)"/>
|
|
||||||
<path d="M68.908 117.492v1.802h5.955v-1.802zm.526.524h4.904v.754h-4.904z" style="color:#fff;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.44935 0 0 1.66414 -74.104 -166.906)"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -1 +0,0 @@
|
|||||||
let apiUri = `${window.location.protocol}//${window.location.host.split(':')[0]}:6531`
|
|
Before Width: | Height: | Size: 66 KiB |
@@ -1,301 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Tranga</title>
|
|
||||||
<link id='basestyle' rel="stylesheet" href="styles/base.css">
|
|
||||||
<link id='librarystyle' rel="stylesheet" href="styles/style_default.css">
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<wrapper>
|
|
||||||
|
|
||||||
<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="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="defaultApiUri.js"></script>
|
|
||||||
<script src="apiConnector.js"></script>
|
|
||||||
<script src="interaction.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,876 +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 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';
|
|
||||||
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();
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
//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();});
|
|
||||||
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 = "";
|
|
||||||
mDexFeedRL.value = "";
|
|
||||||
mDexImageRL.value = "";
|
|
||||||
|
|
||||||
GetSettings().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
settingApiUri.placeholder = apiUri;
|
|
||||||
settingUserAgent.placeholder = 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';
|
|
||||||
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.placeholder = connector.baseUrl;
|
|
||||||
settingKavitaUser.placeholder = "***";
|
|
||||||
settingKavitaPass.placeholder = "***";
|
|
||||||
break;
|
|
||||||
case "Komga":
|
|
||||||
settingKomgaConfigured.setAttribute("configuration", "Active");
|
|
||||||
settingKomgaUrl.placeholder = connector.baseUrl;
|
|
||||||
settingKomgaUser.placeholder = "***";
|
|
||||||
settingKomgaPass.placeholder = "***";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Unknown type");
|
|
||||||
console.log(connector);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
GetNotificationConnectors().then((json) => {
|
|
||||||
json.forEach(connector => {
|
|
||||||
switch(notificationConnectorTypes[connector.notificationConnectorType]){
|
|
||||||
case "Gotify":
|
|
||||||
settingGotifyUrl.placeholder = connector.endpoint;
|
|
||||||
settingGotifyAppToken.placeholder = "***";
|
|
||||||
settingGotifyConfigured.setAttribute("configuration", "Active");
|
|
||||||
break;
|
|
||||||
case "LunaSea":
|
|
||||||
settingLunaseaConfigured.setAttribute("configuration", "Active");
|
|
||||||
settingLunaseaWebhook.placeholder = connector.id;
|
|
||||||
break;
|
|
||||||
case "Ntfy":
|
|
||||||
settingNtfyConfigured.setAttribute("configuration", "Active");
|
|
||||||
settingNtfyEndpoint.placeholder = connector.endpoint;
|
|
||||||
settingNtfyAuth.placeholder = "***";
|
|
||||||
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 (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
|
|
||||||
}
|
|
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="#0F1729"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 804 B |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
width="800px" height="800px" viewBox="0 0 971.986 971.986"
|
|
||||||
xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<path d="M370.216,459.3c10.2,11.1,15.8,25.6,15.8,40.6v442c0,26.601,32.1,40.101,51.1,21.4l123.3-141.3
|
|
||||||
c16.5-19.8,25.6-29.601,25.6-49.2V500c0-15,5.7-29.5,15.8-40.601L955.615,75.5c26.5-28.8,6.101-75.5-33.1-75.5h-873
|
|
||||||
c-39.2,0-59.7,46.6-33.1,75.5L370.216,459.3z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 714 B |
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<!-- 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">
|
|
||||||
|
|
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,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
width="800px" height="800px" viewBox="0 0 93.5 93.5" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path d="M93.5,40.899c0-2.453-1.995-4.447-4.448-4.447H81.98c-0.74-2.545-1.756-5.001-3.035-7.331l4.998-5
|
|
||||||
c0.826-0.827,1.303-1.973,1.303-3.146c0-1.19-0.462-2.306-1.303-3.146L75.67,9.555c-1.613-1.615-4.673-1.618-6.29,0l-5,5
|
|
||||||
c-2.327-1.28-4.786-2.296-7.332-3.037v-7.07C57.048,1.995,55.053,0,52.602,0H40.899c-2.453,0-4.447,1.995-4.447,4.448v7.071
|
|
||||||
c-2.546,0.741-5.005,1.757-7.333,3.037l-5-5c-1.68-1.679-4.609-1.679-6.288,0L9.555,17.83c-1.734,1.734-1.734,4.555,0,6.289
|
|
||||||
l4.999,5c-1.279,2.33-2.295,4.788-3.036,7.333h-7.07C1.995,36.452,0,38.447,0,40.899V52.6c0,2.453,1.995,4.447,4.448,4.447h7.071
|
|
||||||
c0.74,2.545,1.757,5.003,3.036,7.332l-4.998,4.999c-0.827,0.827-1.303,1.974-1.303,3.146c0,1.189,0.462,2.307,1.302,3.146
|
|
||||||
l8.274,8.273c1.614,1.615,4.674,1.619,6.29,0l5-5c2.328,1.279,4.786,2.297,7.333,3.037v7.071c0,2.453,1.995,4.448,4.447,4.448
|
|
||||||
h11.702c2.453,0,4.446-1.995,4.446-4.448V81.98c2.546-0.74,5.005-1.756,7.332-3.037l5,5c1.681,1.68,4.608,1.68,6.288,0
|
|
||||||
l8.275-8.273c1.734-1.734,1.734-4.555,0-6.289l-4.998-5.001c1.279-2.329,2.295-4.787,3.035-7.332h7.071
|
|
||||||
c2.453,0,4.448-1.995,4.448-4.446V40.899z M62.947,46.75c0,8.932-7.266,16.197-16.197,16.197c-8.931,0-16.197-7.266-16.197-16.197
|
|
||||||
c0-8.931,7.266-16.197,16.197-16.197C55.682,30.553,62.947,37.819,62.947,46.75z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,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 |
@@ -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,159 +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::after{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0; top: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 100%; height: 100%;
|
|
||||||
background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-information {
|
|
||||||
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;
|
|
||||||
font-size: 16pt;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 img {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
z-index: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
24
tranga-website/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
54
tranga-website/README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
extends: [
|
||||||
|
// Remove ...tseslint.configs.recommended and replace with this
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
plugins: {
|
||||||
|
// Add the react-x and react-dom plugins
|
||||||
|
'react-x': reactX,
|
||||||
|
'react-dom': reactDom,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended typescript rules
|
||||||
|
...reactX.configs['recommended-typescript'].rules,
|
||||||
|
...reactDom.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
28
tranga-website/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
13
tranga-website/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/blahaj.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Tranga</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
7199
tranga-website/package-lock.json
generated
Normal file
36
tranga-website/package.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "tranga-website",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"@fontsource/inter": "^5.2.5",
|
||||||
|
"@mui/icons-material": "^7.0.1",
|
||||||
|
"@mui/joy": "^5.0.0-beta.52",
|
||||||
|
"@uiw/react-markdown-preview": "^5.1.4",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.21.0",
|
||||||
|
"@types/react": "^19.0.10",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.21.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"globals": "^15.15.0",
|
||||||
|
"swagger-typescript-api": "^13.2.7",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"typescript-eslint": "^8.24.1",
|
||||||
|
"vite": "^6.2.0"
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
13
tranga-website/src/App.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.app {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
}
|
85
tranga-website/src/App.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
import './App.css'
|
||||||
|
import Settings from "./Components/Settings/Settings.tsx";
|
||||||
|
import Header from "./Header.tsx";
|
||||||
|
import {createContext, ReactNode, useEffect, useState} from "react";
|
||||||
|
import {V2} from "./apiClient/V2.ts";
|
||||||
|
import { ApiContext } from './apiClient/ApiContext.tsx';
|
||||||
|
import MangaList from "./Components/Mangas/MangaList.tsx";
|
||||||
|
import {FileLibrary, Manga, MangaConnector} from "./apiClient/data-contracts.ts";
|
||||||
|
import Search from "./Components/Search.tsx";
|
||||||
|
import {Typography} from "@mui/joy";
|
||||||
|
|
||||||
|
export const MangaConnectorContext = createContext<MangaConnector[]>([]);
|
||||||
|
export const MangaContext = createContext<Manga[]>([]);
|
||||||
|
export const FileLibraryContext = createContext<FileLibrary[]>([]);
|
||||||
|
|
||||||
|
export default function App () {
|
||||||
|
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/api";
|
||||||
|
const [apiUri, setApiUri] = useState<string>(apiUriStr);
|
||||||
|
const [Api, setApi] = useState<V2>(new V2({
|
||||||
|
baseUrl: apiUri
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]);
|
||||||
|
const [manga, setManga] = useState<Manga[]>([]);
|
||||||
|
const [fileLibraries, setFileLibraries] = useState<FileLibrary[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Api.mangaConnectorList().then(response => {
|
||||||
|
if (response.ok)
|
||||||
|
setMangaConnectors(response.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
Api.fileLibraryList().then(response => {
|
||||||
|
if (response.ok)
|
||||||
|
setFileLibraries(response.data);
|
||||||
|
})
|
||||||
|
|
||||||
|
Api.queryMangaDownloadingList()
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok)
|
||||||
|
setManga(response.data);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("apiUri", apiUri);
|
||||||
|
if (Api.baseUrl != apiUri)
|
||||||
|
setApi(new V2({
|
||||||
|
baseUrl: apiUri
|
||||||
|
}));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ApiContext.Provider value={Api}>
|
||||||
|
<FileLibraryContext value={fileLibraries}>
|
||||||
|
<MangaConnectorContext.Provider value={mangaConnectors}>
|
||||||
|
<MangaContext.Provider value={manga}>
|
||||||
|
{
|
||||||
|
Api ?
|
||||||
|
<Sheet className={"app"}>
|
||||||
|
<Header>
|
||||||
|
<Settings setApiUri={setApiUri} />
|
||||||
|
</Header>
|
||||||
|
<Sheet className={"app-content"}>
|
||||||
|
<MangaList mangas={manga}>
|
||||||
|
<Search />
|
||||||
|
</MangaList>
|
||||||
|
</Sheet>
|
||||||
|
</Sheet>
|
||||||
|
: <Loading />
|
||||||
|
}
|
||||||
|
</MangaContext.Provider>
|
||||||
|
</MangaConnectorContext.Provider>
|
||||||
|
</FileLibraryContext>
|
||||||
|
</ApiContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Loading () : ReactNode{
|
||||||
|
return (
|
||||||
|
<Typography>Loading</Typography>
|
||||||
|
);
|
||||||
|
}
|
32
tranga-website/src/Components/Loading.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {Close, Done} from "@mui/icons-material";
|
||||||
|
import {CircularProgress, ColorPaletteProp} from "@mui/joy";
|
||||||
|
import {ReactNode} from "react";
|
||||||
|
|
||||||
|
export enum LoadingState {
|
||||||
|
none,
|
||||||
|
loading,
|
||||||
|
success,
|
||||||
|
failure
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StateIndicator(state : LoadingState) : ReactNode {
|
||||||
|
switch (state) {
|
||||||
|
case LoadingState.loading:
|
||||||
|
return (<CircularProgress />);
|
||||||
|
case LoadingState.failure:
|
||||||
|
return (<Close />);
|
||||||
|
case LoadingState.success:
|
||||||
|
return (<Done />);
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StateColor(state : LoadingState) : ColorPaletteProp | undefined {
|
||||||
|
switch (state) {
|
||||||
|
case LoadingState.failure:
|
||||||
|
return "danger";
|
||||||
|
case LoadingState.success:
|
||||||
|
return "success";
|
||||||
|
default: return undefined;
|
||||||
|
}
|
||||||
|
}
|
42
tranga-website/src/Components/MangaConnectorLink.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import {CSSProperties, ReactNode, useContext, useEffect, useRef, useState} from "react";
|
||||||
|
import {ChapterMangaConnectorId, MangaConnector, MangaMangaConnectorId} from "../apiClient/data-contracts.ts";
|
||||||
|
import {Link, Tooltip, Typography} from "@mui/joy";
|
||||||
|
import {MangaConnectorContext} from "../App.tsx";
|
||||||
|
import {ApiContext} from "../apiClient/ApiContext.tsx";
|
||||||
|
|
||||||
|
export default function MangaConnectorLink({MangaConnectorId, imageStyle, printName} : {MangaConnectorId : MangaMangaConnectorId | ChapterMangaConnectorId, imageStyle? : CSSProperties, printName?: boolean}) : ReactNode{
|
||||||
|
const mangaConnectorContext = useContext(MangaConnectorContext);
|
||||||
|
const [mangaConnector, setMangaConnector] = useState<MangaConnector | undefined>(mangaConnectorContext?.find(c => c.name == MangaConnectorId.mangaConnectorName));
|
||||||
|
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const connector = mangaConnectorContext?.find(c => c.name == MangaConnectorId.mangaConnectorName);
|
||||||
|
setMangaConnector(connector);
|
||||||
|
if (imageRef?.current != null)
|
||||||
|
imageRef.current.setHTMLUnsafe(`<img ref=${imageRef} src=${mangaConnector?.iconUrl} style=${imageStyle}/>`);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={<Typography>{MangaConnectorId.mangaConnectorName}: <Link href={MangaConnectorId.websiteUrl as string}>{MangaConnectorId.websiteUrl}</Link></Typography>}>
|
||||||
|
<Link href={MangaConnectorId.websiteUrl as string}>
|
||||||
|
<img ref={imageRef} src={mangaConnector?.iconUrl} style={imageStyle}/>
|
||||||
|
{printName ? <Typography>{mangaConnector?.name}</Typography> : null}
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MangaConnectorLinkFromId({MangaConnectorIdId, imageStyle, printName} : {MangaConnectorIdId: string, imageStyle? : CSSProperties, printName?: boolean}) : ReactNode {
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
|
||||||
|
const [node, setNode] = useState<ReactNode>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Api.queryMangaMangaConnectorIdDetail(MangaConnectorIdId).then(response => {
|
||||||
|
if (response.ok)
|
||||||
|
setNode(<MangaConnectorLink key={response.data.key} MangaConnectorId={response.data} imageStyle={{...imageStyle, width: "25px"}} printName={printName} />);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
21
tranga-website/src/Components/Mangas/MangaCard.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.manga-card {
|
||||||
|
width: 220px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manga-cover-blur {
|
||||||
|
background: linear-gradient(135deg, rgba(245, 169, 184, 0.9) 20%, rgba(91, 206, 250, 0.6));
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);)
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manga-card-badge-icon {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manga-modal {
|
||||||
|
width: 90%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
105
tranga-website/src/Components/Mangas/MangaCard.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardCover,
|
||||||
|
Chip,
|
||||||
|
Link,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
Stack, Tooltip,
|
||||||
|
Typography
|
||||||
|
} from "@mui/joy";
|
||||||
|
import {Manga} from "../../apiClient/data-contracts.ts";
|
||||||
|
import {Dispatch, ReactNode, SetStateAction, useContext, useState} from "react";
|
||||||
|
import "./MangaCard.css";
|
||||||
|
import MangaConnectorBadge from "./MangaConnectorBadge.tsx";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
||||||
|
import MarkdownPreview from '@uiw/react-markdown-preview';
|
||||||
|
import {MangaContext} from "../../App.tsx";
|
||||||
|
import {MangaConnectorLinkFromId} from "../MangaConnectorLink.tsx";
|
||||||
|
|
||||||
|
export function MangaCardFromId({mangaId} : {mangaId: string}) {
|
||||||
|
const mangas = useContext(MangaContext);
|
||||||
|
const manga = mangas.find(manga => manga.key === mangaId);
|
||||||
|
|
||||||
|
return <MangaCard manga={manga} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MangaCard({manga, children} : {manga: Manga | undefined, children? : ReactNode}) {
|
||||||
|
if (manga === undefined)
|
||||||
|
return PlaceHolderCard();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MangaConnectorBadge manga={manga}>
|
||||||
|
<Card className={"manga-card"} onClick={() => setOpen(true)}>
|
||||||
|
<CardCover className={"manga-cover"}>
|
||||||
|
<MangaCover manga={manga} />
|
||||||
|
</CardCover>
|
||||||
|
<CardCover className={"manga-cover-blur"} />
|
||||||
|
<CardContent className={"manga-content"}>
|
||||||
|
<Typography level={"title-lg"}>{manga?.name}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<MangaModal manga={manga} open={open} setOpen={setOpen}>
|
||||||
|
{children}
|
||||||
|
</MangaModal>
|
||||||
|
</MangaConnectorBadge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MangaModal({manga, open, setOpen, children}: {manga: Manga | undefined, open: boolean, setOpen: Dispatch<SetStateAction<boolean>>, children?: ReactNode}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)} className={"manga-modal"}>
|
||||||
|
<ModalDialog style={{width:'100%'}}>
|
||||||
|
<ModalClose />
|
||||||
|
<Tooltip title={<Stack spacing={1}>{manga?.altTitles?.map(title => <Chip>{title.title}</Chip>)}</Stack>}>
|
||||||
|
<Typography level={"h4"} width={"fit-content"}>{manga?.name}</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
<Stack direction={"row"} spacing={2}>
|
||||||
|
<Box key={"Cover"} className={"manga-card"}>
|
||||||
|
<MangaCover manga={manga} />
|
||||||
|
</Box>
|
||||||
|
<Stack key={"Description"} direction={"column"} sx={{width: "calc(100% - 230px)"}}>
|
||||||
|
<Stack key={"Tags"} direction={"row"} flexWrap={"wrap"} useFlexGap spacing={0.5}>
|
||||||
|
{manga?.mangaConnectorIdsIds?.map((idid) => <MangaConnectorLinkFromId key={idid} MangaConnectorIdId={idid} />)}
|
||||||
|
{manga?.mangaTags?.map((tag) => <Chip key={tag.tag}>{tag.tag}</Chip>)}
|
||||||
|
{manga?.links?.map((link) => <Chip key={link.key}><Link href={link.linkUrl}>{link.linkProvider}</Link></Chip>)}
|
||||||
|
</Stack>
|
||||||
|
<Box sx={{flexGrow: 1}}>
|
||||||
|
<MarkdownPreview source={manga?.description} style={{background: "transparent"}}/>
|
||||||
|
</Box>
|
||||||
|
<Stack sx={{justifySelf: "flex-end", alignSelf: "flex-end"}} spacing={2} direction={"row"}>{children}</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PlaceHolderCard(){
|
||||||
|
return (
|
||||||
|
<Badge>
|
||||||
|
<Card className={"manga-card"}>
|
||||||
|
<CardCover className={"manga-cover"}>
|
||||||
|
<img src={"/blahaj.png"} />
|
||||||
|
</CardCover>
|
||||||
|
<CardCover className={"manga-cover-blur"} />
|
||||||
|
</Card>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MangaCover({manga}: {manga: Manga | undefined}) {
|
||||||
|
const api = useContext(ApiContext);
|
||||||
|
const uri = manga ? `${api.baseUrl}/v2/Manga/${manga?.key}/Cover` : "blahaj.png";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img src={uri} style={{width: "100%", height: "100%", objectFit: "cover", borderRadius: "var(--CardCover-radius)"}} />
|
||||||
|
);
|
||||||
|
}
|
14
tranga-website/src/Components/Mangas/MangaConnectorBadge.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Badge } from "@mui/joy";
|
||||||
|
import {Manga} from "../../apiClient/data-contracts.ts";
|
||||||
|
import {ReactElement} from "react";
|
||||||
|
import "./MangaCard.css"
|
||||||
|
import {MangaConnectorLinkFromId} from "../MangaConnectorLink.tsx";
|
||||||
|
|
||||||
|
export default function MangaConnectorBadge ({manga, children} : {manga: Manga, children? : ReactElement<any, any> | ReactElement<any,any>[] | undefined}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge badgeContent={manga.mangaConnectorIdsIds?.map(id => <MangaConnectorLinkFromId key={id} MangaConnectorIdId={id} />)}>
|
||||||
|
{children}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
100
tranga-website/src/Components/Mangas/MangaDownloadDialog.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import {Manga} from "../../apiClient/data-contracts.ts";
|
||||||
|
import {ChangeEvent, Dispatch, ReactNode, useContext, useEffect, useState} from "react";
|
||||||
|
import {Button, Checkbox, Option, Select, Stack, Typography} from "@mui/joy";
|
||||||
|
import Drawer from "@mui/joy/Drawer";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
import {MangaConnectorLinkFromId} from "../MangaConnectorLink.tsx";
|
||||||
|
import Sheet from "@mui/joy/Sheet";
|
||||||
|
import {FileLibraryContext} from "../../App.tsx";
|
||||||
|
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
||||||
|
import {LoadingState, StateIndicator} from "../Loading.tsx";
|
||||||
|
|
||||||
|
export default function ({manga} : {manga: Manga}) : ReactNode{
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => setOpen(true)}>Download</Button>
|
||||||
|
<DownloadDrawer manga={manga} open={open} setOpen={setOpen} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DownloadDrawer({manga, open, setOpen}: {manga: Manga, open: boolean, setOpen: Dispatch<boolean>}): ReactNode {
|
||||||
|
const fileLibraries = useContext(FileLibraryContext);
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
|
||||||
|
const onLibraryChange = (_ :any, value: ({} | null)) => {
|
||||||
|
if (value === undefined)
|
||||||
|
return;
|
||||||
|
Api.mangaChangeLibraryCreate(manga.key as string, value as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalClose />
|
||||||
|
<Sheet sx={{width: "calc(95% - 60px)", margin: "30px"}}>
|
||||||
|
<Typography>Download to Library:</Typography>
|
||||||
|
<Select placeholder={"Library"} onChange={onLibraryChange} value={manga.libraryId}>
|
||||||
|
{fileLibraries?.map(library => (
|
||||||
|
<Option value={library.key} key={library.key}><Typography>{library.libraryName}</Typography> <Typography>({library.basePath})</Typography></Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Typography>Download from:</Typography>
|
||||||
|
<Stack>
|
||||||
|
{manga.mangaConnectorIdsIds?.map(id => <DownloadCheckBox key={id} mangaConnectorIdId={id} />)}
|
||||||
|
</Stack>
|
||||||
|
</Sheet>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DownloadCheckBox({mangaConnectorIdId} : {mangaConnectorIdId : string}) : ReactNode {
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
const [useForDownloading, setUseForDownloading] = useState<boolean>(false);
|
||||||
|
const [loading, setLoading] = useState<LoadingState>(LoadingState.none);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(LoadingState.loading);
|
||||||
|
Api.queryMangaMangaConnectorIdDetail(mangaConnectorIdId).then(response => {
|
||||||
|
if (response.ok){
|
||||||
|
setUseForDownloading(response.data.useForDownload as boolean);
|
||||||
|
setLoading(LoadingState.none);
|
||||||
|
}else
|
||||||
|
setLoading(LoadingState.failure);
|
||||||
|
}).catch(_ => setLoading(LoadingState.failure));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSelected = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setLoading(LoadingState.loading);
|
||||||
|
const val = event.currentTarget.checked;
|
||||||
|
Api.queryMangaMangaConnectorIdDetail(mangaConnectorIdId).then(response => {
|
||||||
|
if (!response.ok){
|
||||||
|
setLoading(LoadingState.failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Api.mangaSetAsDownloadFromCreate(response.data.objId, response.data.mangaConnectorName, val)
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok){
|
||||||
|
setUseForDownloading(val);
|
||||||
|
setLoading(LoadingState.success);
|
||||||
|
}else
|
||||||
|
setLoading(LoadingState.failure);
|
||||||
|
}).catch(_ => setLoading(LoadingState.failure));
|
||||||
|
}).catch(_ => setLoading(LoadingState.failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
indeterminateIcon={StateIndicator(LoadingState.loading)}
|
||||||
|
indeterminate={loading === LoadingState.loading}
|
||||||
|
disabled={loading === LoadingState.loading}
|
||||||
|
checked={useForDownloading}
|
||||||
|
onChange={onSelected}
|
||||||
|
label={
|
||||||
|
<Typography>
|
||||||
|
<MangaConnectorLinkFromId MangaConnectorIdId={mangaConnectorIdId} printName={true} />
|
||||||
|
</Typography>
|
||||||
|
} />
|
||||||
|
);
|
||||||
|
}
|
3
tranga-website/src/Components/Mangas/MangaList.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.manga-list {
|
||||||
|
padding-top: calc(1em * var(--joy-lineHeight-md, 1.5) / 2);;
|
||||||
|
}
|
28
tranga-website/src/Components/Mangas/MangaList.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import {ReactNode} from "react";
|
||||||
|
import {MangaCard} from "./MangaCard.tsx";
|
||||||
|
import {Stack} from "@mui/joy";
|
||||||
|
import "./MangaList.css";
|
||||||
|
import {Manga} from "../../apiClient/data-contracts.ts";
|
||||||
|
import MangaDownloadDialog from "./MangaDownloadDialog.tsx";
|
||||||
|
import MangaMerge from "./MangaMerge.tsx";
|
||||||
|
|
||||||
|
export default function MangaList ({mangas, children} : {mangas: Manga[], children?: ReactNode}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className={"manga-list"} direction={"row"} useFlexGap={true} spacing={2} flexWrap={"wrap"} sx={
|
||||||
|
{
|
||||||
|
mx: 'calc(-1 * var(--ModalDialog-padding))',
|
||||||
|
px: 'var(--ModalDialog-padding)',
|
||||||
|
overflowY: 'scroll'
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
{mangas?.map(manga => (
|
||||||
|
<MangaCard key={manga.key} manga={manga}>
|
||||||
|
<MangaDownloadDialog manga={manga} />
|
||||||
|
<MangaMerge manga={manga} />
|
||||||
|
</MangaCard>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
105
tranga-website/src/Components/Mangas/MangaMerge.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import {ReactNode, useContext, useEffect, useState} from "react";
|
||||||
|
import {Manga} from "../../apiClient/data-contracts.ts";
|
||||||
|
import Drawer from "@mui/joy/Drawer";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
||||||
|
import {MangaCard} from "./MangaCard.tsx";
|
||||||
|
import {Alert, Button, Modal, ModalDialog, Stack, Tooltip, Typography} from "@mui/joy";
|
||||||
|
import {KeyboardDoubleArrowRight, Warning} from "@mui/icons-material";
|
||||||
|
import {LoadingState, StateIndicator} from "../Loading.tsx";
|
||||||
|
|
||||||
|
export default function ({manga} : {manga : Manga | undefined}) : ReactNode {
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
|
||||||
|
const [similar, setSimilar] = useState<Manga[]>([]);
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if (manga === undefined || !open)
|
||||||
|
return;
|
||||||
|
Api.queryMangaSimilarNameList(manga.key as string).then(response => {
|
||||||
|
if (response.ok)
|
||||||
|
Api.mangaWithIDsCreate(response.data).then(response => {
|
||||||
|
if (response.ok)
|
||||||
|
setSimilar(response.data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const exit = (manga : Manga) => {
|
||||||
|
setOpen(false);
|
||||||
|
setSimilar(similar.filter(m => m.key != manga.key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={_ => setOpen(true)}>
|
||||||
|
Merge
|
||||||
|
</Button>
|
||||||
|
<Drawer size={"lg"} open={open} onClose={() => setOpen(false)} anchor={"bottom"}>
|
||||||
|
<ModalClose />
|
||||||
|
<Stack direction={"row"} spacing={2} flexWrap={"wrap"} useFlexGap>
|
||||||
|
{similar.map(similarManga => <MangaCard manga={similarManga}>
|
||||||
|
<ConfirmationModal manga={manga as Manga} similarManga={similarManga} exit={() => exit(similarManga)} />
|
||||||
|
</MangaCard>)}
|
||||||
|
</Stack>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfirmationModal({manga, similarManga, exit} : {manga : Manga, similarManga : Manga, exit: ()=>void}) : ReactNode {
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
|
||||||
|
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
|
||||||
|
|
||||||
|
const merge = () => {
|
||||||
|
setLoadingState(LoadingState.loading);
|
||||||
|
Api.mangaMergeIntoPartialUpdate(manga.key as string, similarManga.key as string).then(response => {
|
||||||
|
if (response.ok){
|
||||||
|
setLoadingState(LoadingState.success);
|
||||||
|
setOpen(false);
|
||||||
|
exit();
|
||||||
|
}else
|
||||||
|
setLoadingState(LoadingState.failure);
|
||||||
|
}).catch(_ => setLoadingState(LoadingState.failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{
|
||||||
|
loadingState == LoadingState.success ?
|
||||||
|
<Alert
|
||||||
|
color="success"
|
||||||
|
startDecorator={StateIndicator(LoadingState.success)}
|
||||||
|
>
|
||||||
|
Merged Successfully!
|
||||||
|
</Alert>
|
||||||
|
:
|
||||||
|
<Button onClick={_ => setOpen(true)}>
|
||||||
|
Merge into
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
<Modal open={open} onClose={_ => setOpen(false)}>
|
||||||
|
<ModalDialog>
|
||||||
|
<ModalClose />
|
||||||
|
<Typography level={"h2"}>Confirm Merge</Typography>
|
||||||
|
<Stack direction={"row"} spacing={3} alignItems={"center"}>
|
||||||
|
<MangaCard manga={manga} />
|
||||||
|
<Typography color={"danger"} level={"h1"}><KeyboardDoubleArrowRight /></Typography>
|
||||||
|
<MangaCard manga={similarManga} />
|
||||||
|
</Stack>
|
||||||
|
<Tooltip title={"THIS CAN NOT BE UNDONE!"}>
|
||||||
|
<Button
|
||||||
|
onClick={merge}
|
||||||
|
disabled={loadingState === LoadingState.loading}
|
||||||
|
endDecorator={StateIndicator(loadingState)}
|
||||||
|
color={"danger"}
|
||||||
|
startDecorator={<Warning />}>
|
||||||
|
Merge
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
</>;
|
||||||
|
}
|
141
tranga-website/src/Components/Search.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import {Dispatch, KeyboardEventHandler, ReactNode, useContext, useState} from "react";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardCover, Chip,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
Step,
|
||||||
|
StepIndicator,
|
||||||
|
Stepper,
|
||||||
|
Typography
|
||||||
|
} from "@mui/joy";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
import {MangaConnectorContext} from "../App.tsx";
|
||||||
|
import {Manga, MangaConnector} from "../apiClient/data-contracts.ts";
|
||||||
|
import MangaList from "./Mangas/MangaList.tsx";
|
||||||
|
import {ApiContext} from "../apiClient/ApiContext.tsx";
|
||||||
|
import {LoadingState, StateColor, StateIndicator} from "./Loading.tsx";
|
||||||
|
|
||||||
|
export default function () : ReactNode {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge badgeContent={"+"}>
|
||||||
|
<Card onClick={() => {if (!open) setOpen(true)}} className={"manga-card"}>
|
||||||
|
<CardCover className={"manga-cover"}>
|
||||||
|
<img src={"/blahaj.png"} />
|
||||||
|
</CardCover>
|
||||||
|
<CardCover className={"manga-cover-blur"} />
|
||||||
|
<CardContent>
|
||||||
|
Add
|
||||||
|
</CardContent>
|
||||||
|
<CardContent>
|
||||||
|
<SearchDialog open={open} setOpen={setOpen} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchDialog ({open, setOpen} : {open: boolean, setOpen: Dispatch<boolean>}) : ReactNode {
|
||||||
|
const mangaConnectors = useContext(MangaConnectorContext);
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
|
||||||
|
const [selectedMangaConnector, setSelectedMangaConnector] = useState<MangaConnector | undefined>(undefined);
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>();
|
||||||
|
const [searchResults, setSearchResults] = useState<Manga[]>([]);
|
||||||
|
|
||||||
|
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
|
||||||
|
|
||||||
|
const doTheSearch = () => {
|
||||||
|
if (searchTerm === undefined || searchTerm.length < 1)
|
||||||
|
return;
|
||||||
|
if (!isUrl(searchTerm) && selectedMangaConnector === undefined)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setLoadingState(LoadingState.loading);
|
||||||
|
|
||||||
|
if (isUrl(searchTerm))
|
||||||
|
Api.searchUrlCreate(searchTerm)
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok){
|
||||||
|
setSearchResults([response.data]);
|
||||||
|
setLoadingState(LoadingState.success);
|
||||||
|
}else
|
||||||
|
setLoadingState(LoadingState.failure);
|
||||||
|
}).catch(() => setLoadingState(LoadingState.failure));
|
||||||
|
else
|
||||||
|
Api.searchDetail(selectedMangaConnector!.name, searchTerm)
|
||||||
|
.then(response => {
|
||||||
|
if(response.ok){
|
||||||
|
setSearchResults(response.data);
|
||||||
|
setLoadingState(LoadingState.success);
|
||||||
|
}else
|
||||||
|
setLoadingState(LoadingState.failure);
|
||||||
|
}).catch(() => setLoadingState(LoadingState.failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUrl = (url: string) => {
|
||||||
|
try{
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
}catch (Error){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyDownCheck : KeyboardEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
doTheSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal sx={{width: "100%", height: "100%"}} open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalDialog sx={{width: "80%"}}>
|
||||||
|
<ModalClose />
|
||||||
|
<Typography level={"h1"}>Search or add Manga</Typography>
|
||||||
|
<Stepper orientation={"vertical"}>
|
||||||
|
<Step indicator={<StepIndicator>1</StepIndicator>}>
|
||||||
|
<Typography>Connector</Typography>
|
||||||
|
<Select
|
||||||
|
placeholder={"Select Connector 'Global' to search on all Connectors"}
|
||||||
|
disabled={loadingState == LoadingState.loading}
|
||||||
|
onChange={(_, v) => setSelectedMangaConnector(v as MangaConnector)}>
|
||||||
|
{mangaConnectors?.map(con => (
|
||||||
|
<Option value={con}>
|
||||||
|
<Typography><img src={con.iconUrl} style={{maxHeight: "var(--Icon-fontSize)"}} />{con.name}</Typography>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Step>
|
||||||
|
<Step indicator={<StepIndicator>2</StepIndicator>}>
|
||||||
|
<Typography>Search</Typography>
|
||||||
|
<Input
|
||||||
|
disabled={loadingState == LoadingState.loading}
|
||||||
|
onKeyDown={keyDownCheck}
|
||||||
|
onChange={(e) => setSearchTerm(e.currentTarget.value)}
|
||||||
|
placeholder={"Enter search-term or supported Connector-Url"}
|
||||||
|
endDecorator={<Button
|
||||||
|
disabled={loadingState == LoadingState.loading}
|
||||||
|
onClick={doTheSearch}
|
||||||
|
endDecorator={StateIndicator(loadingState)}
|
||||||
|
color={StateColor(loadingState)}
|
||||||
|
>Search</Button>}
|
||||||
|
/>
|
||||||
|
</Step>
|
||||||
|
<Step indicator={<StepIndicator>3</StepIndicator>}>
|
||||||
|
<Typography>Result <Chip>{searchResults.length}</Chip></Typography>
|
||||||
|
<MangaList mangas={searchResults} />
|
||||||
|
</Step>
|
||||||
|
</Stepper>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
import {ReactNode, useContext, useState} from "react";
|
||||||
|
import {SettingsContext} from "./Settings.tsx";
|
||||||
|
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
||||||
|
import {Card, ColorPaletteProp, Input, Typography} from "@mui/joy";
|
||||||
|
import * as React from "react";
|
||||||
|
import MarkdownPreview from "@uiw/react-markdown-preview";
|
||||||
|
|
||||||
|
export default function () : ReactNode {
|
||||||
|
const settings = useContext(SettingsContext);
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
|
||||||
|
const [scheme, setScheme] = useState<ColorPaletteProp>("neutral");
|
||||||
|
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const schemeChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
setScheme("warning");
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
Api.settingsChapterNamingSchemePartialUpdate(e.target.value)
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok)
|
||||||
|
setScheme("success");
|
||||||
|
else
|
||||||
|
setScheme("danger");
|
||||||
|
})
|
||||||
|
.catch(() => setScheme("danger"));
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Typography>Chapter Naming Scheme</Typography>
|
||||||
|
<Input color={scheme} defaultValue={settings?.chapterNamingScheme as string} placeholder={"Scheme"} onChange={schemeChanged} />
|
||||||
|
<MarkdownPreview style={{backgroundColor: "transparent"}} source={"Placeholders:\n * %M Obj Name\n * %V Volume\n * %C Chapter\n * %T Title\n * %A Author (first in list)\n * %I Chapter Internal ID\n * %i Obj Internal ID\n * %Y Year (Obj)\n *\n * ?_(...) replace _ with a value from above:\n * Everything inside the braces will only be added if the value of %_ is not null"} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
14
tranga-website/src/Components/Settings/Download.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import {SettingsItem} from "./Settings.tsx";
|
||||||
|
import ImageCompression from "./ImageCompression.tsx";
|
||||||
|
import DownloadLanguage from "./DownloadLanguage.tsx";
|
||||||
|
import ChapterNamingScheme from "./ChapterNamingScheme.tsx";
|
||||||
|
|
||||||
|
export default function (){
|
||||||
|
return (
|
||||||
|
<SettingsItem title={"Download"}>
|
||||||
|
<ImageCompression />
|
||||||
|
<DownloadLanguage />
|
||||||
|
<ChapterNamingScheme />
|
||||||
|
</SettingsItem>
|
||||||
|
)
|
||||||
|
}
|
34
tranga-website/src/Components/Settings/DownloadLanguage.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {ReactNode, useContext, useState} from "react";
|
||||||
|
import {SettingsContext} from "./Settings.tsx";
|
||||||
|
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
||||||
|
import {Card, ColorPaletteProp, Input, Typography} from "@mui/joy";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export default function () : ReactNode {
|
||||||
|
const settings = useContext(SettingsContext);
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
|
||||||
|
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const languageChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
setColor("warning");
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
Api.settingsDownloadLanguagePartialUpdate(e.target.value)
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok)
|
||||||
|
setColor("success");
|
||||||
|
else
|
||||||
|
setColor("danger");
|
||||||
|
})
|
||||||
|
.catch(() => setColor("danger"));
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Typography>Download Language</Typography>
|
||||||
|
<Input color={color} defaultValue={settings?.downloadLanguage as string} placeholder={"Language code (f.e. 'en')"} onChange={languageChanged} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
42
tranga-website/src/Components/Settings/FlareSolverr.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import {ReactNode, useCallback, useContext, useState} from "react";
|
||||||
|
import {SettingsContext} from "./Settings.tsx";
|
||||||
|
import {Button, Card, ColorPaletteProp, Input, Typography} from "@mui/joy";
|
||||||
|
import * as React from "react";
|
||||||
|
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
||||||
|
|
||||||
|
export default function () : ReactNode {
|
||||||
|
const settings = useContext(SettingsContext);
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
|
||||||
|
const [uriValue, setUriValue] = React.useState<string>(settings?.flareSolverrUrl as string);
|
||||||
|
const [uriColor, setUriColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const uriChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
setUriValue(e.target.value);
|
||||||
|
setUriColor("warning");
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
changeUri();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeUri = useCallback(() => {
|
||||||
|
Api.settingsFlareSolverrUrlCreate(uriValue)
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok)
|
||||||
|
setUriColor("success");
|
||||||
|
else
|
||||||
|
setUriColor("danger");
|
||||||
|
})
|
||||||
|
.catch(() => setUriColor("danger"));
|
||||||
|
}, [uriValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Typography>FlareSolverr</Typography>
|
||||||
|
<Input color={uriColor} value={uriValue} type={"url"} placeholder={"URL"} onChange={uriChanged}
|
||||||
|
endDecorator={<Button onClick={changeUri}>Apply</Button>}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
41
tranga-website/src/Components/Settings/ImageCompression.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {ReactNode, useCallback, useContext, useState} from "react";
|
||||||
|
import {SettingsContext} from "./Settings.tsx";
|
||||||
|
import {Button, Card, ColorPaletteProp, Slider, Typography} from "@mui/joy";
|
||||||
|
import * as React from "react";
|
||||||
|
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
||||||
|
|
||||||
|
export default function () : ReactNode {
|
||||||
|
const settings = useContext(SettingsContext);
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
|
||||||
|
const [value, setValue] = useState(settings?.imageCompression??100);
|
||||||
|
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const valueChanged = (_: Event, val: number | number[]) => {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
setColor("warning");
|
||||||
|
setValue(val as number);
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
changeUri();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeUri = useCallback(() => {
|
||||||
|
Api.settingsImageCompressionLevelPartialUpdate(value)
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok)
|
||||||
|
setColor("success");
|
||||||
|
else
|
||||||
|
setColor("danger");
|
||||||
|
})
|
||||||
|
.catch(() => setColor("danger"));
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Typography>Image Compression</Typography>
|
||||||
|
<Slider sx={{marginTop: "20px"}} valueLabelDisplay={"auto"} value={value} onChange={valueChanged} color={color} />
|
||||||
|
<Button onClick={changeUri}>Set</Button>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
import {Modal, ModalDialog, Tab, TabList, Tabs} from "@mui/joy";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export default function ({open, setOpen} : {open: boolean, setOpen: Dispatch<boolean>}) {
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalDialog>
|
||||||
|
<ModalClose />
|
||||||
|
<Tabs sx={{width:'95%'}} defaultValue={"komga"}>
|
||||||
|
<TabList>
|
||||||
|
<Tab value={"komga"}>Komga</Tab>
|
||||||
|
<Tab value={"kavita"}>Kavita</Tab>
|
||||||
|
</TabList>
|
||||||
|
</Tabs>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
import {Button, Card, Typography} from "@mui/joy";
|
||||||
|
import {useState} from "react";
|
||||||
|
import ListLibraryConnectors from "./ListLibraryConnectors.tsx";
|
||||||
|
import AddLibraryConnector from "./AddLibraryConnector.tsx";
|
||||||
|
|
||||||
|
export default function (){
|
||||||
|
|
||||||
|
const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Typography>Library Connectors</Typography>
|
||||||
|
<Button onClick={() => setAddDialogOpen(true)}>Add</Button>
|
||||||
|
<ListLibraryConnectors />
|
||||||
|
<AddLibraryConnector open={addDialogOpen} setOpen={() => setAddDialogOpen(false)} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
import {useContext, useEffect, useState} from "react";
|
||||||
|
import {ApiContext} from "../../../apiClient/ApiContext.tsx";
|
||||||
|
import {LibraryConnector} from "../../../apiClient/data-contracts.ts";
|
||||||
|
import {Card, Chip, Input, Stack} from "@mui/joy";
|
||||||
|
|
||||||
|
export default function (){
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
const [libraryConnectors, setLibraryConnectors] = useState<LibraryConnector[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getConnectors();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getConnectors = () => {
|
||||||
|
Api.libraryConnectorList().then(r => {
|
||||||
|
if(r.ok)
|
||||||
|
setLibraryConnectors(r.data);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction={"column"} spacing={1}>
|
||||||
|
{libraryConnectors.map(c => <LibraryConnectorItem key={c.key} connector={c} />)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LibraryConnectorItem({connector} : {connector: LibraryConnector}){
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Chip>{connector.libraryType}</Chip>
|
||||||
|
<Input disabled value={connector.baseUrl} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
31
tranga-website/src/Components/Settings/Maintenance.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {Button} from "@mui/joy";
|
||||||
|
import {SettingsItem} from "./Settings.tsx";
|
||||||
|
import {useContext, useState} from "react";
|
||||||
|
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
||||||
|
import {LoadingState, StateColor, StateIndicator} from "../Loading.tsx";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
|
||||||
|
const [unusedMangaState, setUnusedMangaState] = useState(LoadingState.none);
|
||||||
|
const cleanUnusedManga = () => {
|
||||||
|
setUnusedMangaState(LoadingState.loading);
|
||||||
|
Api.maintenanceCleanupNoDownloadMangaCreate()
|
||||||
|
.then(r => {
|
||||||
|
if (r.ok)
|
||||||
|
setUnusedMangaState(LoadingState.success);
|
||||||
|
else
|
||||||
|
setUnusedMangaState(LoadingState.failure);
|
||||||
|
}).catch(_ => setUnusedMangaState(LoadingState.failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsItem title={"Maintenance"}>
|
||||||
|
<Button
|
||||||
|
disabled={unusedMangaState == LoadingState.loading}
|
||||||
|
color={StateColor(unusedMangaState)}
|
||||||
|
endDecorator={StateIndicator(unusedMangaState)}
|
||||||
|
onClick={cleanUnusedManga}>Cleanup unused Manga</Button>
|
||||||
|
</SettingsItem>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,141 @@
|
|||||||
|
import {ReactNode, useContext, useState} from "react";
|
||||||
|
import { ApiContext } from "../../../apiClient/ApiContext.tsx";
|
||||||
|
import {
|
||||||
|
Button, Card,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
Stack,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs, Typography
|
||||||
|
} from "@mui/joy";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
import {GotifyRecord, NtfyRecord, PushoverRecord} from "../../../apiClient/data-contracts.ts";
|
||||||
|
import {LoadingState, StateColor, StateIndicator} from "../../Loading.tsx";
|
||||||
|
import * as React from "react";
|
||||||
|
import ListNotificationConnector from "./ListNotificationConnector.tsx";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const [notificationConnectorsOpen, setNotificationConnectorsOpen] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Typography>Notification Connectors</Typography>
|
||||||
|
<Button onClick={() => setNotificationConnectorsOpen(true)}>Add</Button>
|
||||||
|
<ListNotificationConnector />
|
||||||
|
<Modal open={notificationConnectorsOpen} onClose={() => setNotificationConnectorsOpen(false)}>
|
||||||
|
<ModalDialog>
|
||||||
|
<ModalClose />
|
||||||
|
<Tabs sx={{width:'95%'}} defaultValue={"gotify"}>
|
||||||
|
<TabList>
|
||||||
|
<Tab value={"gotify"}>Gotify</Tab>
|
||||||
|
<Tab value={"ntfy"}>Ntfy</Tab>
|
||||||
|
<Tab value={"pushover"}>Pushover</Tab>
|
||||||
|
</TabList>
|
||||||
|
<Gotify />
|
||||||
|
<Ntfy />
|
||||||
|
<Pushover />
|
||||||
|
</Tabs>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationConnectorTab({ value, children, add, state }: { value: string, children: ReactNode, add: (data: any) => void, state: LoadingState }) {
|
||||||
|
|
||||||
|
const IsLoading = (state : LoadingState) : boolean => state === LoadingState.loading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabPanel value={value}>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
{children}
|
||||||
|
<Button onClick={add} endDecorator={StateIndicator(state)} loading={IsLoading(state)} disabled={IsLoading(state)} color={StateColor(state)}>Add</Button>
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Gotify() {
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
const [gotifyData, setGotifyData] = useState<GotifyRecord>({});
|
||||||
|
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
|
||||||
|
|
||||||
|
const Add = () => {
|
||||||
|
setLoadingState(LoadingState.loading);
|
||||||
|
Api.notificationConnectorGotifyUpdate(gotifyData)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok)
|
||||||
|
setLoadingState(LoadingState.success);
|
||||||
|
else
|
||||||
|
setLoadingState(LoadingState.failure);
|
||||||
|
})
|
||||||
|
.catch(_ => setLoadingState(LoadingState.failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationConnectorTab value={"gotify"} add={Add} state={loadingState}>
|
||||||
|
<Input placeholder={"Name"} value={gotifyData.name as string} onChange={(e) => setGotifyData({...gotifyData, name: e.target.value})} />
|
||||||
|
<Input placeholder={"https://[...]/message"} value={gotifyData.endpoint as string} onChange={(e) => setGotifyData({...gotifyData, endpoint: e.target.value})} />
|
||||||
|
<Input placeholder={"Apptoken"} type={"password"} value={gotifyData.appToken as string} onChange={(e) => setGotifyData({...gotifyData, appToken: e.target.value})} />
|
||||||
|
<Input placeholder={"Priority"} type={"number"} value={gotifyData.priority as number} onChange={(e) => setGotifyData({...gotifyData, priority: e.target.valueAsNumber})} />
|
||||||
|
</NotificationConnectorTab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ntfy() {
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
const [ntfyData, setNtfyData] = useState<NtfyRecord>({});
|
||||||
|
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
|
||||||
|
|
||||||
|
const Add = () => {
|
||||||
|
setLoadingState(LoadingState.loading);
|
||||||
|
Api.notificationConnectorNtfyUpdate(ntfyData)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok)
|
||||||
|
setLoadingState(LoadingState.success);
|
||||||
|
else
|
||||||
|
setLoadingState(LoadingState.failure);
|
||||||
|
})
|
||||||
|
.catch(_ => setLoadingState(LoadingState.failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationConnectorTab value={"ntfy"} add={Add} state={loadingState}>
|
||||||
|
<Input placeholder={"Name"} value={ntfyData.name as string} onChange={(e) => setNtfyData({...ntfyData, name: e.target.value})} />
|
||||||
|
<Input placeholder={"Endpoint"} value={ntfyData.endpoint as string} onChange={(e) => setNtfyData({...ntfyData, endpoint: e.target.value})} />
|
||||||
|
<Input placeholder={"Topic"} value={ntfyData.topic as string} onChange={(e) => setNtfyData({...ntfyData, topic: e.target.value})} />
|
||||||
|
<Input placeholder={"Username"} value={ntfyData.username as string} onChange={(e) => setNtfyData({...ntfyData, username: e.target.value})} />
|
||||||
|
<Input placeholder={"Password"} type={"password"} value={ntfyData.password as string} onChange={(e) => setNtfyData({...ntfyData, password: e.target.value})} />
|
||||||
|
<Input placeholder={"Priority"} type={"number"} value={ntfyData.priority as number} onChange={(e) => setNtfyData({...ntfyData, priority: e.target.valueAsNumber})} />
|
||||||
|
</NotificationConnectorTab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Pushover() {
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
const [pushoverData, setPushoverData] = useState<PushoverRecord>({});
|
||||||
|
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
|
||||||
|
|
||||||
|
const Add = () => {
|
||||||
|
setLoadingState(LoadingState.loading);
|
||||||
|
Api.notificationConnectorPushoverUpdate(pushoverData)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok)
|
||||||
|
setLoadingState(LoadingState.success);
|
||||||
|
else
|
||||||
|
setLoadingState(LoadingState.failure);
|
||||||
|
})
|
||||||
|
.catch(_ => setLoadingState(LoadingState.failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationConnectorTab value={"pushover"} add={Add} state={loadingState}>
|
||||||
|
<Input placeholder={"Name"} value={pushoverData.name as string} onChange={(e) => setPushoverData({...pushoverData, name: e.target.value})} />
|
||||||
|
<Input placeholder={"User"} value={pushoverData.user as string} onChange={(e) => setPushoverData({...pushoverData, user: e.target.value})} />
|
||||||
|
<Input placeholder={"AppToken"} type={"password"} value={pushoverData.appToken as string} onChange={(e) => setPushoverData({...pushoverData, appToken: e.target.value})} />
|
||||||
|
</NotificationConnectorTab>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,52 @@
|
|||||||
|
import {ApiContext} from "../../../apiClient/ApiContext.tsx";
|
||||||
|
import {useContext, useEffect, useState} from "react";
|
||||||
|
import { NotificationConnector } from "../../../apiClient/data-contracts.ts";
|
||||||
|
import {Card, Chip, Input, Stack, Table, Textarea, Typography} from "@mui/joy";
|
||||||
|
|
||||||
|
export default function (){
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
const [notificationConnectors, setNotificationConnectors] = useState<NotificationConnector[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getConnectors();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getConnectors = () => {
|
||||||
|
Api.notificationConnectorList().then(r => {
|
||||||
|
if(r.ok)
|
||||||
|
setNotificationConnectors(r.data);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction={"column"} spacing={1}>
|
||||||
|
{notificationConnectors.map(c => <NotificationConnectorItem key={c.name} connector={c} />)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationConnectorItem({connector} : {connector: NotificationConnector}){
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Typography left={"h2"}>{connector.name}</Typography>
|
||||||
|
<Input disabled startDecorator={<Chip>{connector.httpMethod}</Chip>} value={connector.url} />
|
||||||
|
<Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Header</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(connector.headers).map(x =>
|
||||||
|
<tr key={x[0]}>
|
||||||
|
<td>{x[0]}</td>
|
||||||
|
<td>{[x[1]]}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
<Textarea disabled value={connector.body}/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
17
tranga-website/src/Components/Settings/Services.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {SettingsItem} from "./Settings.tsx";
|
||||||
|
import NotificationConnectors from "./NotificationConnectors/AddNotificationConnector.tsx";
|
||||||
|
import FlareSolverr from "./FlareSolverr.tsx";
|
||||||
|
import LibraryConnectors from "./LibraryConnectors/LibraryConnectors.tsx";
|
||||||
|
|
||||||
|
export default function(){
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsItem title={"Services"} direction={"row"}>
|
||||||
|
<FlareSolverr />
|
||||||
|
<NotificationConnectors />
|
||||||
|
<LibraryConnectors />
|
||||||
|
</SettingsItem>
|
||||||
|
);
|
||||||
|
}
|
0
tranga-website/src/Components/Settings/Settings.css
Normal file
98
tranga-website/src/Components/Settings/Settings.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import ModalClose from '@mui/joy/ModalClose';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionGroup,
|
||||||
|
AccordionSummary, Button, ColorPaletteProp,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle, Input,
|
||||||
|
Modal, ModalDialog, Stack
|
||||||
|
} from "@mui/joy";
|
||||||
|
import './Settings.css';
|
||||||
|
import * as React from "react";
|
||||||
|
import {createContext, Dispatch, ReactNode, useContext, useEffect, useState} from "react";
|
||||||
|
import {TrangaSettings} from "../../apiClient/data-contracts.ts";
|
||||||
|
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
||||||
|
import {SxProps} from "@mui/joy/styles/types";
|
||||||
|
import Maintenance from "./Maintenance.tsx";
|
||||||
|
import Services from "./Services.tsx";
|
||||||
|
import Download from './Download.tsx';
|
||||||
|
|
||||||
|
export const SettingsContext = createContext<TrangaSettings|undefined>(undefined);
|
||||||
|
|
||||||
|
export default function Settings({setApiUri} : {setApiUri: Dispatch<React.SetStateAction<string>>}) {
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
const [settings, setSettings] = useState<TrangaSettings>();
|
||||||
|
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const [apiUriColor, setApiUriColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Api.settingsList().then((response) => {
|
||||||
|
setSettings(response.data);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const apiUriChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
setApiUriColor("warning");
|
||||||
|
|
||||||
|
const val = e.target.value;
|
||||||
|
const value = val.endsWith("/") ? val.substring(0, val.length - 1) : val;
|
||||||
|
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
setApiUri(value);
|
||||||
|
setApiUriColor("success");
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalStyle : SxProps = {
|
||||||
|
width: "80%",
|
||||||
|
height: "80%"
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContext.Provider value={settings}>
|
||||||
|
<Button onClick={() => setOpen(true)}>Settings</Button>
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalDialog sx={ModalStyle}>
|
||||||
|
<ModalClose />
|
||||||
|
<DialogTitle>Settings</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<AccordionGroup>
|
||||||
|
<SettingsItem title={"ApiUri"}>
|
||||||
|
<Input
|
||||||
|
color={apiUriColor}
|
||||||
|
placeholder={"http(s)://"}
|
||||||
|
type={"url"}
|
||||||
|
defaultValue={Api.baseUrl}
|
||||||
|
onChange={apiUriChanged} />
|
||||||
|
</SettingsItem>
|
||||||
|
<Download />
|
||||||
|
<Services />
|
||||||
|
<Maintenance />
|
||||||
|
</AccordionGroup>
|
||||||
|
</DialogContent>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
</SettingsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsItem({title, children, defaultExpanded, direction} : {title: string, children?: ReactNode, defaultExpanded?: boolean, direction?: "row" | "column"}) {
|
||||||
|
const [expanded, setExpanded] = React.useState(defaultExpanded??false);
|
||||||
|
const stackDirection = direction ?? "column";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion expanded={expanded} onChange={(_, expanded) => setExpanded(expanded)}>
|
||||||
|
<AccordionSummary>{title}</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Stack direction={stackDirection} spacing={1}>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
11
tranga-website/src/Header.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.header {
|
||||||
|
position: sticky !important;
|
||||||
|
z-index: 1000;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
}
|
35
tranga-website/src/Header.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import Sheet from "@mui/joy/Sheet";
|
||||||
|
import {Link, Stack, Typography} from "@mui/joy";
|
||||||
|
import {ReactElement, useContext} from "react";
|
||||||
|
import './Header.css';
|
||||||
|
import {Article, GitHub} from "@mui/icons-material";
|
||||||
|
import {ApiContext} from "./apiClient/ApiContext.tsx";
|
||||||
|
|
||||||
|
export default function Header({children} : {children? : ReactElement<any, any> | ReactElement<any,any>[] | undefined}) : ReactElement {
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet className={"header"}>
|
||||||
|
<Stack direction={"row"} spacing={2} sx={{width: "100%", alignItems: "center", justifyContent: "space-between"}} useFlexGap>
|
||||||
|
<Stack sx={{flexGrow: 1, flexBasis: 1}} direction={"row"} spacing={2}>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
<Stack sx={{flexGrow: 1, height: "100%", flexBasis: 1, justifyContent: "center"}} direction={"row"}>
|
||||||
|
<img src={"/blahaj.png"} style={{cursor: "grab", maxHeight: "100%"}}/>
|
||||||
|
<Typography level={"h2"} sx={{
|
||||||
|
background: "linear-gradient(110deg, var(--joy-palette-primary-solidBg), var(--joy-palette-success-400))",
|
||||||
|
WebkitBackgroundClip: "text",
|
||||||
|
WebkitTextFillColor: "transparent",
|
||||||
|
fontWeight: "bold",
|
||||||
|
cursor: "default"
|
||||||
|
}}>Tranga</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack sx={{flexGrow: 1, flexBasis: 1, justifyContent: "flex-end"}} direction={"row"} spacing={2}>
|
||||||
|
<Link target={"_blank"} href={"https://github.com/C9Glax/tranga"} color={"neutral"} height={"min-content"} ><GitHub /> Server</Link>
|
||||||
|
<Link target={"_blank"} href={"https://github.com/C9Glax/tranga-website"} color={"neutral"} height={"min-content"} ><GitHub /> Website</Link>
|
||||||
|
<Link target={"_blank"} href={Api.baseUrl + "/swagger"} color={"neutral"} height={"min-content"} ><Article />Swagger</Link>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
4
tranga-website/src/apiClient/ApiContext.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
import {V2} from "./V2.ts";
|
||||||
|
|
||||||
|
export const ApiContext = createContext<V2>(new V2());
|
32
tranga-website/src/apiClient/CleanupNoDownloadManga.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||||
|
* ## ##
|
||||||
|
* ## AUTHOR: acacode ##
|
||||||
|
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { HttpClient, RequestParams } from "./http-client";
|
||||||
|
|
||||||
|
export class CleanupNoDownloadManga<
|
||||||
|
SecurityDataType = unknown,
|
||||||
|
> extends HttpClient<SecurityDataType> {
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Maintenance
|
||||||
|
* @name CleanupNoDownloadMangaCreate
|
||||||
|
* @summary Removes all API.Schema.MangaContext.Manga not marked for Download on any API.MangaConnectors.MangaConnector
|
||||||
|
* @request POST:/CleanupNoDownloadManga
|
||||||
|
*/
|
||||||
|
cleanupNoDownloadMangaCreate = (params: RequestParams = {}) =>
|
||||||
|
this.request<void, string>({
|
||||||
|
path: `/CleanupNoDownloadManga`,
|
||||||
|
method: "POST",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
31
tranga-website/src/apiClient/SettingsContext.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {createContext, useContext, useState} from "react";
|
||||||
|
import {TrangaSettings} from "./data-contracts.ts";
|
||||||
|
import {ApiContext} from "./ApiContext.tsx";
|
||||||
|
|
||||||
|
const [settingsPromise, setSettingsPromise] = useState<Promise<TrangaSettings | undefined>>();
|
||||||
|
const [settings, setSettings] = useState<TrangaSettings>();
|
||||||
|
|
||||||
|
const API = useContext(ApiContext);
|
||||||
|
|
||||||
|
export const SettingsContext = createContext<{ GetSettings: () => Promise<TrangaSettings | undefined> }>(
|
||||||
|
{
|
||||||
|
GetSettings: () : Promise<TrangaSettings | undefined> => {
|
||||||
|
const promise = settingsPromise;
|
||||||
|
if(promise) return promise;
|
||||||
|
const p = new Promise<TrangaSettings | undefined>((resolve, reject) => {
|
||||||
|
if (settings) resolve(settings);
|
||||||
|
|
||||||
|
console.log(`Fetching settings`);
|
||||||
|
API.settingsList()
|
||||||
|
.then(result => {
|
||||||
|
if (!result.ok)
|
||||||
|
throw new Error(`Error fetching settings`);
|
||||||
|
setSettings(result.data);
|
||||||
|
resolve(result.data);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
setSettingsPromise(p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
1388
tranga-website/src/apiClient/V2.ts
Normal file
388
tranga-website/src/apiClient/data-contracts.ts
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||||
|
* ## ##
|
||||||
|
* ## AUTHOR: acacode ##
|
||||||
|
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum WorkerExecutionState {
|
||||||
|
Failed = "Failed",
|
||||||
|
Cancelled = "Cancelled",
|
||||||
|
Created = "Created",
|
||||||
|
Waiting = "Waiting",
|
||||||
|
Running = "Running",
|
||||||
|
Completed = "Completed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RequestType {
|
||||||
|
Default = "Default",
|
||||||
|
MangaDexFeed = "MangaDexFeed",
|
||||||
|
MangaImage = "MangaImage",
|
||||||
|
MangaCover = "MangaCover",
|
||||||
|
MangaDexImage = "MangaDexImage",
|
||||||
|
MangaInfo = "MangaInfo",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MangaReleaseStatus {
|
||||||
|
Continuing = "Continuing",
|
||||||
|
Completed = "Completed",
|
||||||
|
OnHiatus = "OnHiatus",
|
||||||
|
Cancelled = "Cancelled",
|
||||||
|
Unreleased = "Unreleased",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LibraryType {
|
||||||
|
Komga = "Komga",
|
||||||
|
Kavita = "Kavita",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AltTitle {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 8
|
||||||
|
*/
|
||||||
|
language: string;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
key?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Author {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 128
|
||||||
|
*/
|
||||||
|
authorName: string;
|
||||||
|
key?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseWorker {
|
||||||
|
/** Workers this Worker depends on being completed before running. */
|
||||||
|
dependsOn?: BaseWorker[] | null;
|
||||||
|
/** Dependencies and dependencies of dependencies. See also API.Workers.BaseWorker.DependsOn. */
|
||||||
|
allDependencies?: BaseWorker[] | null;
|
||||||
|
/** API.Workers.BaseWorker.AllDependencies and Self. */
|
||||||
|
dependenciesAndSelf?: BaseWorker[] | null;
|
||||||
|
/** API.Workers.BaseWorker.DependsOn where API.Workers.WorkerExecutionState is less than Completed. */
|
||||||
|
missingDependencies?: BaseWorker[] | null;
|
||||||
|
allDependenciesFulfilled?: boolean;
|
||||||
|
key?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Chapter {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
parentMangaId: string;
|
||||||
|
idsOnMangaConnectors?: Record<string, string>;
|
||||||
|
/** @format int32 */
|
||||||
|
volumeNumber?: number | null;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 10
|
||||||
|
*/
|
||||||
|
chapterNumber: string;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
title?: string | null;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
fileName: string;
|
||||||
|
downloaded: boolean;
|
||||||
|
fullArchiveFilePath?: string | null;
|
||||||
|
key?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChapterMangaConnectorId {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
objId: string;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 32
|
||||||
|
*/
|
||||||
|
mangaConnectorName: string;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
idOnConnectorSite: string;
|
||||||
|
/**
|
||||||
|
* @format uri
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 512
|
||||||
|
*/
|
||||||
|
websiteUrl?: string | null;
|
||||||
|
useForDownload?: boolean;
|
||||||
|
key?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileLibrary {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
basePath: string;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 512
|
||||||
|
*/
|
||||||
|
libraryName: string;
|
||||||
|
key?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GotifyRecord {
|
||||||
|
name?: string | null;
|
||||||
|
endpoint?: string | null;
|
||||||
|
appToken?: string | null;
|
||||||
|
/** @format int32 */
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryConnector {
|
||||||
|
libraryType: LibraryType;
|
||||||
|
/**
|
||||||
|
* @format uri
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
baseUrl: string;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
auth: string;
|
||||||
|
key?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Link {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
linkProvider: string;
|
||||||
|
/**
|
||||||
|
* @format uri
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 2048
|
||||||
|
*/
|
||||||
|
linkUrl: string;
|
||||||
|
key?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Manga {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 512
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/** @minLength 1 */
|
||||||
|
description: string;
|
||||||
|
releaseStatus: MangaReleaseStatus;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
libraryId?: string | null;
|
||||||
|
authors?: Author[] | null;
|
||||||
|
mangaTags?: MangaTag[] | null;
|
||||||
|
links?: Link[] | null;
|
||||||
|
altTitles?: AltTitle[] | null;
|
||||||
|
/** @format float */
|
||||||
|
ignoreChaptersBefore: number;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 1024
|
||||||
|
*/
|
||||||
|
directoryName: string;
|
||||||
|
/** @format int32 */
|
||||||
|
year?: number | null;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 8
|
||||||
|
*/
|
||||||
|
originalLanguage?: string | null;
|
||||||
|
chapterIds?: string[] | null;
|
||||||
|
idsOnMangaConnectors?: Record<string, string>;
|
||||||
|
mangaConnectorIdsIds?: string[] | null;
|
||||||
|
key?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MangaConnector {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 32
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 8
|
||||||
|
*/
|
||||||
|
supportedLanguages: string[];
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 2048
|
||||||
|
*/
|
||||||
|
iconUrl: string;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
baseUris: string[];
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MangaMangaConnectorId {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
objId: string;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 32
|
||||||
|
*/
|
||||||
|
mangaConnectorName: string;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
idOnConnectorSite: string;
|
||||||
|
/**
|
||||||
|
* @format uri
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 512
|
||||||
|
*/
|
||||||
|
websiteUrl?: string | null;
|
||||||
|
useForDownload?: boolean;
|
||||||
|
key?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MangaTag {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataEntry {
|
||||||
|
mangaId?: string | null;
|
||||||
|
metadataFetcherName?: string | null;
|
||||||
|
identifier?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataSearchResult {
|
||||||
|
identifier?: string | null;
|
||||||
|
name?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
coverUrl?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationConnector {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* @format uri
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 2048
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 8
|
||||||
|
*/
|
||||||
|
httpMethod: string;
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 4096
|
||||||
|
*/
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NtfyRecord {
|
||||||
|
name?: string | null;
|
||||||
|
endpoint?: string | null;
|
||||||
|
username?: string | null;
|
||||||
|
password?: string | null;
|
||||||
|
topic?: string | null;
|
||||||
|
/** @format int32 */
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProblemDetails {
|
||||||
|
type?: string | null;
|
||||||
|
title?: string | null;
|
||||||
|
/** @format int32 */
|
||||||
|
status?: number | null;
|
||||||
|
detail?: string | null;
|
||||||
|
instance?: string | null;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PushoverRecord {
|
||||||
|
name?: string | null;
|
||||||
|
appToken?: string | null;
|
||||||
|
user?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrangaSettings {
|
||||||
|
downloadLocation?: string | null;
|
||||||
|
userAgent?: string | null;
|
||||||
|
/** @format int32 */
|
||||||
|
imageCompression?: number;
|
||||||
|
blackWhiteImages?: boolean;
|
||||||
|
flareSolverrUrl?: string | null;
|
||||||
|
/**
|
||||||
|
* Placeholders:
|
||||||
|
* %M Obj Name
|
||||||
|
* %V Volume
|
||||||
|
* %C Chapter
|
||||||
|
* %T Title
|
||||||
|
* %A Author (first in list)
|
||||||
|
* %I Chapter Internal ID
|
||||||
|
* %i Obj Internal ID
|
||||||
|
* %Y Year (Obj)
|
||||||
|
*
|
||||||
|
* ?_(...) replace _ with a value from above:
|
||||||
|
* Everything inside the braces will only be added if the value of %_ is not null
|
||||||
|
*/
|
||||||
|
chapterNamingScheme?: string | null;
|
||||||
|
/** @format int32 */
|
||||||
|
workCycleTimeoutMs?: number;
|
||||||
|
requestLimits?: {
|
||||||
|
/** @format int32 */
|
||||||
|
Default?: number;
|
||||||
|
/** @format int32 */
|
||||||
|
MangaDexFeed?: number;
|
||||||
|
/** @format int32 */
|
||||||
|
MangaImage?: number;
|
||||||
|
/** @format int32 */
|
||||||
|
MangaCover?: number;
|
||||||
|
/** @format int32 */
|
||||||
|
MangaDexImage?: number;
|
||||||
|
/** @format int32 */
|
||||||
|
MangaInfo?: number;
|
||||||
|
} | null;
|
||||||
|
downloadLanguage?: string | null;
|
||||||
|
}
|
260
tranga-website/src/apiClient/http-client.ts
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||||
|
* ## ##
|
||||||
|
* ## AUTHOR: acacode ##
|
||||||
|
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type QueryParamsType = Record<string | number, any>;
|
||||||
|
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
|
||||||
|
|
||||||
|
export interface FullRequestParams extends Omit<RequestInit, "body"> {
|
||||||
|
/** set parameter to `true` for call `securityWorker` for this request */
|
||||||
|
secure?: boolean;
|
||||||
|
/** request path */
|
||||||
|
path: string;
|
||||||
|
/** content type of request body */
|
||||||
|
type?: ContentType;
|
||||||
|
/** query params */
|
||||||
|
query?: QueryParamsType;
|
||||||
|
/** format of response (i.e. response.json() -> format: "json") */
|
||||||
|
format?: ResponseFormat;
|
||||||
|
/** request body */
|
||||||
|
body?: unknown;
|
||||||
|
/** base url */
|
||||||
|
baseUrl?: string;
|
||||||
|
/** request cancellation token */
|
||||||
|
cancelToken?: CancelToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestParams = Omit<
|
||||||
|
FullRequestParams,
|
||||||
|
"body" | "method" | "query" | "path"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface ApiConfig<SecurityDataType = unknown> {
|
||||||
|
baseUrl?: string;
|
||||||
|
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
|
||||||
|
securityWorker?: (
|
||||||
|
securityData: SecurityDataType | null,
|
||||||
|
) => Promise<RequestParams | void> | RequestParams | void;
|
||||||
|
customFetch?: typeof fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
|
||||||
|
extends Response {
|
||||||
|
data: D;
|
||||||
|
error: E;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CancelToken = Symbol | string | number;
|
||||||
|
|
||||||
|
export enum ContentType {
|
||||||
|
Json = "application/json",
|
||||||
|
JsonApi = "application/vnd.api+json",
|
||||||
|
FormData = "multipart/form-data",
|
||||||
|
UrlEncoded = "application/x-www-form-urlencoded",
|
||||||
|
Text = "text/plain",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpClient<SecurityDataType = unknown> {
|
||||||
|
public baseUrl: string = "";
|
||||||
|
private securityData: SecurityDataType | null = null;
|
||||||
|
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
||||||
|
private abortControllers = new Map<CancelToken, AbortController>();
|
||||||
|
private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
|
||||||
|
fetch(...fetchParams);
|
||||||
|
|
||||||
|
private baseApiParams: RequestParams = {
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {},
|
||||||
|
redirect: "follow",
|
||||||
|
referrerPolicy: "no-referrer",
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
|
||||||
|
Object.assign(this, apiConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSecurityData = (data: SecurityDataType | null) => {
|
||||||
|
this.securityData = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected encodeQueryParam(key: string, value: any) {
|
||||||
|
const encodedKey = encodeURIComponent(key);
|
||||||
|
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addQueryParam(query: QueryParamsType, key: string) {
|
||||||
|
return this.encodeQueryParam(key, query[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addArrayQueryParam(query: QueryParamsType, key: string) {
|
||||||
|
const value = query[key];
|
||||||
|
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toQueryString(rawQuery?: QueryParamsType): string {
|
||||||
|
const query = rawQuery || {};
|
||||||
|
const keys = Object.keys(query).filter(
|
||||||
|
(key) => "undefined" !== typeof query[key],
|
||||||
|
);
|
||||||
|
return keys
|
||||||
|
.map((key) =>
|
||||||
|
Array.isArray(query[key])
|
||||||
|
? this.addArrayQueryParam(query, key)
|
||||||
|
: this.addQueryParam(query, key),
|
||||||
|
)
|
||||||
|
.join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
||||||
|
const queryString = this.toQueryString(rawQuery);
|
||||||
|
return queryString ? `?${queryString}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
||||||
|
[ContentType.Json]: (input: any) =>
|
||||||
|
input !== null && (typeof input === "object" || typeof input === "string")
|
||||||
|
? JSON.stringify(input)
|
||||||
|
: input,
|
||||||
|
[ContentType.JsonApi]: (input: any) =>
|
||||||
|
input !== null && (typeof input === "object" || typeof input === "string")
|
||||||
|
? JSON.stringify(input)
|
||||||
|
: input,
|
||||||
|
[ContentType.Text]: (input: any) =>
|
||||||
|
input !== null && typeof input !== "string"
|
||||||
|
? JSON.stringify(input)
|
||||||
|
: input,
|
||||||
|
[ContentType.FormData]: (input: any) =>
|
||||||
|
Object.keys(input || {}).reduce((formData, key) => {
|
||||||
|
const property = input[key];
|
||||||
|
formData.append(
|
||||||
|
key,
|
||||||
|
property instanceof Blob
|
||||||
|
? property
|
||||||
|
: typeof property === "object" && property !== null
|
||||||
|
? JSON.stringify(property)
|
||||||
|
: `${property}`,
|
||||||
|
);
|
||||||
|
return formData;
|
||||||
|
}, new FormData()),
|
||||||
|
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
||||||
|
};
|
||||||
|
|
||||||
|
protected mergeRequestParams(
|
||||||
|
params1: RequestParams,
|
||||||
|
params2?: RequestParams,
|
||||||
|
): RequestParams {
|
||||||
|
return {
|
||||||
|
...this.baseApiParams,
|
||||||
|
...params1,
|
||||||
|
...(params2 || {}),
|
||||||
|
headers: {
|
||||||
|
...(this.baseApiParams.headers || {}),
|
||||||
|
...(params1.headers || {}),
|
||||||
|
...((params2 && params2.headers) || {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createAbortSignal = (
|
||||||
|
cancelToken: CancelToken,
|
||||||
|
): AbortSignal | undefined => {
|
||||||
|
if (this.abortControllers.has(cancelToken)) {
|
||||||
|
const abortController = this.abortControllers.get(cancelToken);
|
||||||
|
if (abortController) {
|
||||||
|
return abortController.signal;
|
||||||
|
}
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
this.abortControllers.set(cancelToken, abortController);
|
||||||
|
return abortController.signal;
|
||||||
|
};
|
||||||
|
|
||||||
|
public abortRequest = (cancelToken: CancelToken) => {
|
||||||
|
const abortController = this.abortControllers.get(cancelToken);
|
||||||
|
|
||||||
|
if (abortController) {
|
||||||
|
abortController.abort();
|
||||||
|
this.abortControllers.delete(cancelToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public request = async <T = any, E = any>({
|
||||||
|
body,
|
||||||
|
secure,
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
query,
|
||||||
|
format,
|
||||||
|
baseUrl,
|
||||||
|
cancelToken,
|
||||||
|
...params
|
||||||
|
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
|
||||||
|
const secureParams =
|
||||||
|
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
|
||||||
|
this.securityWorker &&
|
||||||
|
(await this.securityWorker(this.securityData))) ||
|
||||||
|
{};
|
||||||
|
const requestParams = this.mergeRequestParams(params, secureParams);
|
||||||
|
const queryString = query && this.toQueryString(query);
|
||||||
|
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
|
||||||
|
const responseFormat = format || requestParams.format;
|
||||||
|
|
||||||
|
return this.customFetch(
|
||||||
|
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
|
||||||
|
{
|
||||||
|
...requestParams,
|
||||||
|
headers: {
|
||||||
|
...(requestParams.headers || {}),
|
||||||
|
...(type && type !== ContentType.FormData
|
||||||
|
? { "Content-Type": type }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
signal:
|
||||||
|
(cancelToken
|
||||||
|
? this.createAbortSignal(cancelToken)
|
||||||
|
: requestParams.signal) || null,
|
||||||
|
body:
|
||||||
|
typeof body === "undefined" || body === null
|
||||||
|
? null
|
||||||
|
: payloadFormatter(body),
|
||||||
|
},
|
||||||
|
).then(async (response) => {
|
||||||
|
const r = response.clone() as HttpResponse<T, E>;
|
||||||
|
r.data = null as unknown as T;
|
||||||
|
r.error = null as unknown as E;
|
||||||
|
|
||||||
|
const data = !responseFormat
|
||||||
|
? r
|
||||||
|
: await response[responseFormat]()
|
||||||
|
.then((data) => {
|
||||||
|
if (r.ok) {
|
||||||
|
r.data = data;
|
||||||
|
} else {
|
||||||
|
r.error = data;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
r.error = e;
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cancelToken) {
|
||||||
|
this.abortControllers.delete(cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) throw data;
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
0
tranga-website/src/index.css
Normal file
29
tranga-website/src/main.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
// @ts-ignore
|
||||||
|
import '@fontsource/inter';
|
||||||
|
import { CssVarsProvider } from '@mui/joy/styles';
|
||||||
|
import CssBaseline from '@mui/joy/CssBaseline';
|
||||||
|
import {StrictMode} from "react";
|
||||||
|
import {trangaTheme} from "./theme.ts";
|
||||||
|
|
||||||
|
export default function MyApp() {
|
||||||
|
return (
|
||||||
|
<StrictMode>
|
||||||
|
<CssVarsProvider theme={trangaTheme}>
|
||||||
|
{/* must be used under CssVarsProvider */}
|
||||||
|
<CssBaseline />
|
||||||
|
|
||||||
|
{/* The rest of your application */}
|
||||||
|
<App />
|
||||||
|
</CssVarsProvider>
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<MyApp />
|
||||||
|
);
|
88
tranga-website/src/theme.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { extendTheme } from '@mui/joy/styles';
|
||||||
|
|
||||||
|
|
||||||
|
export const trangaTheme = extendTheme({
|
||||||
|
"colorSchemes": {
|
||||||
|
"light": {
|
||||||
|
"palette": {
|
||||||
|
"primary": {
|
||||||
|
"50": "#FCE5EA",
|
||||||
|
"100": "#FBDDE3",
|
||||||
|
"200": "#F9CBD4",
|
||||||
|
"300": "#F7BAC6",
|
||||||
|
"400": "#F5A9B8",
|
||||||
|
"500": "#F5A9B8",
|
||||||
|
"600": "#C48793",
|
||||||
|
"700": "#AC7681",
|
||||||
|
"800": "#93656E",
|
||||||
|
"900": "#7B555C"
|
||||||
|
},
|
||||||
|
"neutral": {
|
||||||
|
"50": "#E6E6E6",
|
||||||
|
"100": "#CCCCCC",
|
||||||
|
"200": "#B3B3B3",
|
||||||
|
"300": "#999999",
|
||||||
|
"400": "#808080",
|
||||||
|
"500": "#666666",
|
||||||
|
"600": "#4C4C4C",
|
||||||
|
"700": "#333333",
|
||||||
|
"800": "#191919",
|
||||||
|
"900": "#000",
|
||||||
|
"plainColor": "var(--joy-palette-neutral-50)",
|
||||||
|
"plainHoverBg": "var(--joy-palette-neutral-700)",
|
||||||
|
"outlinedColor": "var(--joy-palette-neutral-50)",
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"50": "#cef0fe",
|
||||||
|
"100": "#bdebfd",
|
||||||
|
"200": "#9de2fc",
|
||||||
|
"300": "#7cd8fb",
|
||||||
|
"400": "#5bcefa",
|
||||||
|
"500": "#5bcefa",
|
||||||
|
"600": "#49a5c8",
|
||||||
|
"700": "#4090af",
|
||||||
|
"800": "#2e677d",
|
||||||
|
"900": "#245264"
|
||||||
|
},
|
||||||
|
"danger": {
|
||||||
|
"50": "#f2c0b3",
|
||||||
|
"100": "#ea9680",
|
||||||
|
"200": "#e68166",
|
||||||
|
"300": "#dd5733",
|
||||||
|
"400": "#d52d00",
|
||||||
|
"500": "#d52d00",
|
||||||
|
"600": "#aa2400",
|
||||||
|
"700": "#951f00",
|
||||||
|
"800": "#6b1700",
|
||||||
|
"900": "#400d00"
|
||||||
|
},
|
||||||
|
"warning": {
|
||||||
|
"50": "#ffebdd",
|
||||||
|
"100": "#ffd7bb",
|
||||||
|
"200": "#ffc29a",
|
||||||
|
"300": "#ffae78",
|
||||||
|
"400": "#ff9a56",
|
||||||
|
"500": "#ff9a56",
|
||||||
|
"600": "#cc7b45",
|
||||||
|
"700": "#995c34",
|
||||||
|
"800": "#663e22",
|
||||||
|
"900": "#331f11"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"body": "var(--joy-palette-neutral-900)",
|
||||||
|
"surface": "var(--joy-palette-neutral-900)",
|
||||||
|
"popup": "var(--joy-palette-neutral-800)"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"primary": "var(--joy-palette-neutral-50)",
|
||||||
|
"secondary": "var(--joy-palette-success-200)",
|
||||||
|
"tertiary": "var(--joy-palette-primary-200)",
|
||||||
|
"icon": "var(--joy-palette-primary-50)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"palette": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
1
tranga-website/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
26
tranga-website/tsconfig.app.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
7
tranga-website/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
24
tranga-website/tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
10
tranga-website/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
}
|
||||||
|
})
|