Compare commits
286 Commits
6952bcaa1d
...
vite-react
Author | SHA1 | Date | |
---|---|---|---|
b8350602c5 | |||
b4e25a9f54 | |||
5c78896a5d | |||
6bee507d93 | |||
7ec740da82 | |||
e253ae3d20 | |||
84f9dc176e | |||
84e9d70d34 | |||
95f4086c24 | |||
4c81c571a4 | |||
b4b4479e9b | |||
d6eeae4021 | |||
6b05c4fb00 | |||
d785b6e078 | |||
e64f325022 | |||
486f6f60fb | |||
01c732f6a8 | |||
e5641e0690 | |||
b7ee80b4a4 | |||
9f8d874822 | |||
a273af5ed9 | |||
be704d922a | |||
692bf3561b | |||
e0ec64882b | |||
496e6fdde8 | |||
49fe38962d | |||
645d3c8793 | |||
2ef7ee6b39 | |||
e1b590482c | |||
94e58d2d1b | |||
d5c85efcb2 | |||
aed0a562f0 | |||
46729bade3 | |||
fed7ff987e | |||
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 | |||
594103689e | |||
6ab431fde0 | |||
bbad467fe6 | |||
03c78f7b6a | |||
3ad7fbfad2 | |||
6173d16edc | |||
8a07818b89 | |||
6aa53f7ca7 | |||
415e6c606f | |||
178e8b72f3 | |||
65457c289f | |||
6109bb678a | |||
cc14c5adad | |||
8c0f61fc6a | |||
82389c4847 | |||
a91452ee8c | |||
698e16cc2b | |||
4e20b95d77 | |||
18257cd584 | |||
5626a9d0fd | |||
c6dc3cd107 | |||
94c47b904d | |||
10f48af9fa | |||
5e95c34306 | |||
66dcb2f1e6 | |||
19e033995d | |||
99265bacb2 | |||
5385dfd918 | |||
d6e4d1d27f | |||
288cd77049 | |||
e605578a34 | |||
6340c5ad03 | |||
6d402357e7 | |||
9e0eb0262e | |||
63b220bd79 | |||
d480f62e51 | |||
44755675e5 | |||
6b10aa8926 | |||
48b669dc07 | |||
fbb25807e6 | |||
44f43b7152 | |||
68653778c8 | |||
fd4c01203f | |||
2a4316cf1d | |||
d358611133 | |||
b0cbb416d6 | |||
e3ad70c0a9 | |||
f38290b2ed | |||
211db3d4d5 | |||
a3046680ac | |||
1ed376ba47 | |||
63b10600da | |||
5333c025f7 | |||
187dd22027 | |||
2092db2ba3 | |||
ecd76712d8 | |||
60f957ede2 | |||
fcd232bec9 | |||
357fca3bd5 | |||
1d8dd7381d | |||
0b0abb3801 | |||
62665f5660 | |||
007b49c624 | |||
d9bbbed1c0 | |||
42a1e1a2ce | |||
aad18c0195 | |||
1eef710efc | |||
84520d8e18 | |||
89586ef891 | |||
33514ee375 | |||
526e129fb7 | |||
d1dae83387 | |||
9199a7a0e4 | |||
d9fa4185e2 | |||
717c8cd33e | |||
483c2d564b | |||
5807844281 | |||
77d9e6eee1 | |||
e45e7bd5f5 | |||
ccab3a8027 | |||
5eca8dac5d | |||
0b34ca7723 | |||
eb943ccbed | |||
514cd06d2b | |||
2035bc8f4e | |||
ece175d1db | |||
da3867962b | |||
ec15732e57 | |||
6d10c81dff | |||
a3842ba20f | |||
74b71d57b7 | |||
6a5e340882 | |||
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 | |||
95801828c6 | |||
3f26d3bbd6 | |||
daa05a0b4d | |||
2f9eb61377 | |||
65814dd942 | |||
dbad993c7a | |||
ac8ca1f886 | |||
f27f11e7c2 | |||
9d8dadc634 | |||
ca091bac92 | |||
cf09bc50fb | |||
d5115809ca | |||
b70dfbf482 | |||
0bc93a7e5c | |||
d4efb158ef | |||
fba5764328 | |||
101a269042 | |||
3c45813c61 | |||
cb38939888 | |||
eb48cf3733 | |||
ddd9512469 | |||
c26e208822 | |||
c656e5face | |||
4addacb229 | |||
c1aece8200 | |||
6bd9a0b1db | |||
c72e152a7e | |||
8eda0dcb1d | |||
38572a3a46 | |||
5b52577610 | |||
3b99dbd487 | |||
b36cbe1d90 | |||
3eb7b63fa6 | |||
793561dad7 | |||
acb22d770d | |||
e0093c65ff | |||
ecf9e1a243 | |||
5af3005179 | |||
d33ac16c7d | |||
a2e0a2375a | |||
9c0a7a0a50 | |||
d05b8ea76b | |||
e9937c02bf | |||
ef5524d7b6 | |||
8f9700ec02 | |||
4b86cd7104 | |||
61e3fbd500 | |||
657a3d1857 | |||
b4cf22b6eb | |||
2216e13c49 | |||
0a68da2b49 | |||
b87c159823 | |||
6fac397e36 | |||
31a03a54bb | |||
83ed99e5db | |||
5cda6ece05 | |||
b99ade74c7 | |||
4f6b07280a | |||
01067006f8 | |||
4c418add81 | |||
1ee17360e5 | |||
9accca0ad2 | |||
9309a4e28f | |||
c75f65f3d5 | |||
c7e49fb6bc | |||
2c5a032890 | |||
8bd6ad4c2b | |||
148af6abaa | |||
1ba49ddf08 | |||
f50990dafb | |||
76f6216501 | |||
18d3a09d93 | |||
0bc3147bb3 | |||
43cbb80eec | |||
95876f1a53 | |||
42caebf458 | |||
ddee459aa2 | |||
4cd06bd0e0 | |||
d138094a66 |
12
.github/workflows/docker-image-cuttingedge.yml
vendored
@ -3,6 +3,8 @@ name: Docker Image CI
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "cuttingedge" ]
|
branches: [ "cuttingedge" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "cuttingedge" ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -17,12 +19,12 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/setup-qemu-action#usage
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2.2.0
|
uses: docker/setup-qemu-action@v3.6.0
|
||||||
|
|
||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2.10.0
|
uses: docker/setup-buildx-action@v3.11.1
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -33,12 +35,12 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push Website
|
- name: Build and push Website
|
||||||
uses: docker/build-push-action@v4.1.1
|
uses: docker/build-push-action@v6.18.0
|
||||||
with:
|
with:
|
||||||
context: ./Website
|
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: |
|
||||||
|
12
.github/workflows/docker-image-master.yml
vendored
@ -3,8 +3,6 @@ name: Docker Image CI
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
pull_request:
|
|
||||||
branches: [ "master" ]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -19,12 +17,12 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/setup-qemu-action#usage
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2.2.0
|
uses: docker/setup-qemu-action@v3.6.0
|
||||||
|
|
||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2.10.0
|
uses: docker/setup-buildx-action@v3.11.1
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -35,12 +33,12 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push Website
|
- name: Build and push Website
|
||||||
uses: docker/build-push-action@v4.1.1
|
uses: docker/build-push-action@v6.18.0
|
||||||
with:
|
with:
|
||||||
context: ./Website
|
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,7 +2,7 @@ name: Docker Image CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "cuttingedge" ]
|
branches: [ "vite-react-ts" ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -17,12 +17,12 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/setup-qemu-action#usage
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2.2.0
|
uses: docker/setup-qemu-action@v3.6.0
|
||||||
|
|
||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2.10.0
|
uses: docker/setup-buildx-action@v3.11.1
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -33,13 +33,13 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push Website
|
- name: Build and push Website
|
||||||
uses: docker/build-push-action@v4.1.1
|
uses: docker/build-push-action@v6.18.0
|
||||||
with:
|
with:
|
||||||
context: ./Website
|
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: |
|
||||||
glax/tranga-website:dev
|
glax/tranga-website:Server-V2
|
9
.gitignore
vendored
@ -18,4 +18,11 @@ riderModule.iml
|
|||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
/.idea
|
/.idea
|
||||||
cover.jpg
|
cover.jpg
|
||||||
cover.png
|
cover.png
|
||||||
|
.vs/slnx.sqlite
|
||||||
|
.vs/tranga-website/config/applicationhost.config
|
||||||
|
.vs/tranga-website/FileContentIndex/91a465d3-1190-42e0-95eb-fa3694744e58.vsidx
|
||||||
|
.vs/tranga-website/v17/.wsuo
|
||||||
|
.vs/VSWorkspaceState.json
|
||||||
|
/node_modules/
|
||||||
|
/.vite/
|
||||||
|
19
Dockerfile
@ -1,4 +1,19 @@
|
|||||||
FROM nginx:alpine3.17-slim
|
# Build stage
|
||||||
COPY . /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
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
ENV API_URL=http://tranga-api:6531
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
105
README.md
@ -1,16 +1,6 @@
|
|||||||
<!-- PROJECT SHIELDS -->
|
|
||||||
<!--
|
|
||||||
*** I'm using markdown "reference style" links for readability.
|
|
||||||
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
|
|
||||||
*** See the bottom of this document for the declaration of the reference variables
|
|
||||||
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
|
|
||||||
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- 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">
|
||||||
@ -19,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>
|
||||||
@ -65,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,210 +0,0 @@
|
|||||||
let apiUri = `http://${window.location.host.split(':')[0]}:6531`
|
|
||||||
|
|
||||||
if(getCookie("apiUri") != ""){
|
|
||||||
apiUri = getCookie("apiUri");
|
|
||||||
}
|
|
||||||
setCookie("apiUri", apiUri);
|
|
||||||
|
|
||||||
function setCookie(cname, cvalue) {
|
|
||||||
const d = new Date();
|
|
||||||
d.setTime(d.getTime() + (365*24*60*60*1000));
|
|
||||||
let expires = "expires="+ d.toUTCString();
|
|
||||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/;samesite=strict";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCookie(cname) {
|
|
||||||
let name = cname + "=";
|
|
||||||
let decodedCookie = decodeURIComponent(document.cookie);
|
|
||||||
let ca = decodedCookie.split(';');
|
|
||||||
for(let i = 0; i < ca.length; i++) {
|
|
||||||
let c = ca[i];
|
|
||||||
while (c.charAt(0) == ' ') {
|
|
||||||
c = c.substring(1);
|
|
||||||
}
|
|
||||||
if (c.indexOf(name) == 0) {
|
|
||||||
return c.substring(name.length, c.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetData(uri){
|
|
||||||
let request = await fetch(uri, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let json = await request.json();
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PostData(uri){
|
|
||||||
fetch(uri, {
|
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ChangeStyleSheet(sheet){
|
|
||||||
var uri = `${apiUri}/Settings/ChangeStyleSheet?styleSheet=${sheet}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateKomga(komgaUrl, komgaAuth){
|
|
||||||
var uri = `${apiUri}/LibraryConnectors/Update?libraryConnector=Komga&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateKavita(kavitaUrl, kavitaUsername, kavitaPassword){
|
|
||||||
var uri = `${apiUri}/LibraryConnectors/Update?libraryConnector=Kavita&kavitaUrl=${kavitaUrl}&kavitaUsername=${kavitaUsername}&kavitaPassword=${kavitaPassword}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateGotify(gotifyUrl, gotifyAppToken){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Update?notificationConnector=Gotify&gotifyUrl=${gotifyUrl}&gotifyAppToken=${gotifyAppToken}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateLunaSea(lunaseaWebhook){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Update?notificationConnector=LunaSea&lunaseaWebhook=${lunaseaWebhook}`;
|
|
||||||
PostData(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateNtfy(ntfyEndpoint, ntfyAuth){
|
|
||||||
var uri = `${apiUri}/NotificationConnectors/Update?notificationConnector=Ntfy&ntfyUrl=${ntfyEndpoint}&ntfyAuth=${ntfyAuth}`;
|
|
||||||
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);
|
|
||||||
return json;
|
|
||||||
}
|
|
Before Width: | Height: | Size: 66 KiB |
@ -1,160 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Tranga</title>
|
|
||||||
<link id='pagestyle' 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>
|
|
||||||
<searchdiv>
|
|
||||||
<label for="searchbox"></label><input id="searchbox" placeholder="Filter" type="text">
|
|
||||||
</searchdiv>
|
|
||||||
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
|
|
||||||
</topbar>
|
|
||||||
<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>
|
|
||||||
<popup-title>Settings</popup-title>
|
|
||||||
<popup-content>
|
|
||||||
<div>
|
|
||||||
<p class="title">Download Location:</p>
|
|
||||||
<span id="downloadLocation"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="title">API-URI</p>
|
|
||||||
<label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="title">Komga</span>
|
|
||||||
<div>Configured: <span id="komgaConfigured">✅❌</span></div>
|
|
||||||
<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>
|
|
||||||
<div>
|
|
||||||
<span class="title">Kavita</span>
|
|
||||||
<div>Configured: <span id="kavitaConfigured">✅❌</span></div>
|
|
||||||
<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>
|
|
||||||
<div>
|
|
||||||
<span class="title">Gotify</span>
|
|
||||||
<div>Configured: <span id="gotifyConfigured">✅❌</span></div>
|
|
||||||
<label for="gotifyUrl"></label><input placeholder="URL" id="gotifyUrl" type="text">
|
|
||||||
<label for="gotifyAppToken"></label><input placeholder="App-Token" id="gotifyAppToken" type="text">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="title">LunaSea</span>
|
|
||||||
<div>Configured: <span id="lunaseaConfigured">✅❌</span></div>
|
|
||||||
<label for="lunaseaWebhook"></label><input placeholder="device/:id or user/:id" id="lunaseaWebhook" type="text">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="title">Ntfy</span>
|
|
||||||
<div>Configured: <span id="ntfyConfigured">✅❌</span></div>
|
|
||||||
<label for="ntfyEndpoint"></label><input placeholder="URL" id="ntfyEndpoint" type="text">
|
|
||||||
<label for="ntfyAuth"></label><input placeholder="Auth" id="ntfyAuth" type="text">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input type="checkbox" id="mangaHoverCheckbox" name="css-style" value="style_mangahover.css" onclick="updateCSS()">
|
|
||||||
<label for="css-style"> Show manga titles and sources on hover</label><br>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input type="submit" value="Update" onclick="UpdateSettings()">
|
|
||||||
</div>
|
|
||||||
</popup-content>
|
|
||||||
</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="blurBackgroundJobStatus" onclick="jobStatusView.style.display= 'none';"></blur-background>
|
|
||||||
<popup-window>
|
|
||||||
<div>
|
|
||||||
<div id="jobStatusRunning" style="border-right: 1px solid gray;"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div id="jobStatusWaiting" style="border-left: 1px solid gray;"></div>
|
|
||||||
</div>
|
|
||||||
</popup-window>
|
|
||||||
</popup>
|
|
||||||
</viewport>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div onclick="ShowJobQueue();">
|
|
||||||
<img src="media/running.svg" alt="running"><div id="jobsRunningTag">0</div>
|
|
||||||
</div>
|
|
||||||
<div onclick="ShowJobQueue();">
|
|
||||||
<img src="media/queue.svg" alt="queue"><div id="jobsQueuedTag">0</div>
|
|
||||||
</div>
|
|
||||||
<p id="madeWith">Made with Blåhaj 🦈</p>
|
|
||||||
</footer>
|
|
||||||
</wrapper>
|
|
||||||
|
|
||||||
<script src="apiConnector.js"></script>
|
|
||||||
<script src="interaction.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,609 +0,0 @@
|
|||||||
let monitoringJobsCount = 0;
|
|
||||||
let runningJobs = [];
|
|
||||||
let waitingJobs = [];
|
|
||||||
let notificationConnectorTypes = [];
|
|
||||||
let libraryConnectorTypes = [];
|
|
||||||
let selectedManga;
|
|
||||||
let selectedJob;
|
|
||||||
|
|
||||||
const searchBox = document.querySelector("#searchbox");
|
|
||||||
const settingsPopup = document.querySelector("#settingsPopup");
|
|
||||||
const settingsCog = document.querySelector("#settingscog");
|
|
||||||
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");
|
|
||||||
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");
|
|
||||||
const settingDownloadLocation = document.querySelector("#downloadLocation");
|
|
||||||
const settingKomgaUrl = document.querySelector("#komgaUrl");
|
|
||||||
const settingKomgaUser = document.querySelector("#komgaUsername");
|
|
||||||
const settingKomgaPass = document.querySelector("#komgaPassword");
|
|
||||||
const settingKavitaUrl = document.querySelector("#kavitaUrl");
|
|
||||||
const settingKavitaUser = document.querySelector("#kavitaUsername");
|
|
||||||
const settingKavitaPass = document.querySelector("#kavitaPassword");
|
|
||||||
const settingGotifyUrl = document.querySelector("#gotifyUrl");
|
|
||||||
const settingGotifyAppToken = document.querySelector("#gotifyAppToken");
|
|
||||||
const settingLunaseaWebhook = document.querySelector("#lunaseaWebhook");
|
|
||||||
const settingNtfyEndpoint = document.querySelector("#ntfyEndpoint");
|
|
||||||
const settingNtfyAuth = document.querySelector("#ntfyAuth");
|
|
||||||
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 settingApiUri = document.querySelector("#settingApiUri");
|
|
||||||
const settingMangaHoverCheckbox = document.querySelector("#mangaHoverCheckbox");
|
|
||||||
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");
|
|
||||||
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) => {
|
|
||||||
json.forEach(connector => {
|
|
||||||
notificationConnectorTypes[connector.Key] = connector.Value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
GetAvailableLibraryConnectors().then((json) => {
|
|
||||||
json.forEach(connector => {
|
|
||||||
libraryConnectorTypes[connector.Key] = connector.Value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
GetAvailableControllers().then((json) => {
|
|
||||||
newMangaConnector.replaceChildren();
|
|
||||||
json.forEach(connector => {
|
|
||||||
var option = document.createElement('option');
|
|
||||||
option.value = connector;
|
|
||||||
option.innerText = connector;
|
|
||||||
newMangaConnector.appendChild(option);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ResetContent();
|
|
||||||
UpdateJobs();
|
|
||||||
GetSettings().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
settingDownloadLocation.innerText = json.downloadLocation;
|
|
||||||
settingApiUri.placeholder = apiUri;
|
|
||||||
//console.log(json.styleSheet);
|
|
||||||
if (json.styleSheet == 'hover') {
|
|
||||||
settingMangaHoverCheckbox.checked = true;
|
|
||||||
document.getElementById('pagestyle').setAttribute('href', 'styles/style_mangahover.css');
|
|
||||||
} else {
|
|
||||||
settingMangaHoverCheckbox.checked = false;
|
|
||||||
document.getElementById('pagestyle').setAttribute('href', 'styles/style_default.css');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setInterval(() => {
|
|
||||||
UpdateJobs();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Setup();
|
|
||||||
|
|
||||||
function updateCSS(){
|
|
||||||
if (document.getElementById("mangaHoverCheckbox").checked == true){
|
|
||||||
ChangeStyleSheet('hover')
|
|
||||||
document.getElementById('pagestyle').setAttribute('href', 'styles/style_mangahover.css');
|
|
||||||
//console.log('Changing theme to mangahover')
|
|
||||||
} else {
|
|
||||||
ChangeStyleSheet('default');
|
|
||||||
document.getElementById('pagestyle').setAttribute('href', 'styles/style_default.css');
|
|
||||||
//console.log('Changing theme to default')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
var statusTooltip = document.createElement('status-tooltip');
|
|
||||||
if (manga.releaseStatus == 0) {
|
|
||||||
releaseStatus.style.backgroundColor = 'limegreen';
|
|
||||||
statusTooltip.innerText = "Ongoing"
|
|
||||||
}else if(manga.releaseStatus == 1){
|
|
||||||
releaseStatus.style.backgroundColor = 'blueviolet';
|
|
||||||
statusTooltip.innerText = "Completed"
|
|
||||||
} else if (manga.releaseStatus == 2) {
|
|
||||||
releaseStatus.style.backgroundColor = 'darkorange';
|
|
||||||
statusTooltip.innerText = "On Hiatus"
|
|
||||||
} else if (manga.releaseStatus == 3) {
|
|
||||||
releaseStatus.style.backgroundColor = 'firebrick';
|
|
||||||
statusTooltip.innerText = "Cancelled"
|
|
||||||
} else if (manga.releaseStatus == 4) {
|
|
||||||
releaseStatus.style.backgroundColor = 'aqua';
|
|
||||||
statusTooltip.innerText = "Upcoming";
|
|
||||||
} else {
|
|
||||||
releaseStatus.style.backgroundColor = 'gray';
|
|
||||||
statusTooltip.innerText = 'Status Unavailable';
|
|
||||||
}
|
|
||||||
releaseStatus.appendChild(statusTooltip); //Append the tooltip to the indicator circle
|
|
||||||
mangaName.appendChild(releaseStatus); //Append the release status indicator to the info block
|
|
||||||
|
|
||||||
info.appendChild(mangaName);
|
|
||||||
mangaElement.appendChild(info);
|
|
||||||
return mangaElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
createMonitorJobButton.addEventListener("click", () => {
|
|
||||||
CreateMonitorJob(newMangaConnector.value, selectedManga.internalId, newMangaTranslatedLanguage.value);
|
|
||||||
UpdateJobs();
|
|
||||||
mangaViewerPopup.style.display = "none";
|
|
||||||
ResetContent();
|
|
||||||
});
|
|
||||||
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";
|
|
||||||
ResetContent();
|
|
||||||
});
|
|
||||||
|
|
||||||
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(){
|
|
||||||
if(searchBox.value.length > 0){
|
|
||||||
tasksContent.childNodes.forEach(publication => {
|
|
||||||
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())){
|
|
||||||
publication.style.display = "none";
|
|
||||||
}else{
|
|
||||||
publication.style.display = "initial";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
tasksContent.childNodes.forEach(publication => publication.style.display = "initial");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsCog.addEventListener("click", () => {
|
|
||||||
OpenSettings();
|
|
||||||
settingsPopup.style.display = "flex";
|
|
||||||
});
|
|
||||||
|
|
||||||
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(); });
|
|
||||||
settingApiUri.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
|
|
||||||
|
|
||||||
function OpenSettings(){
|
|
||||||
settingGotifyConfigured.innerText = "❌";
|
|
||||||
settingLunaseaConfigured.innerText = "❌";
|
|
||||||
settingNtfyConfigured.innerText = "❌";
|
|
||||||
settingKavitaConfigured.innerText = "❌";
|
|
||||||
settingKomgaConfigured.innerText = "❌";
|
|
||||||
settingKomgaUrl.value = "";
|
|
||||||
settingKomgaUser.value = "";
|
|
||||||
settingKomgaPass.value = "";
|
|
||||||
settingKavitaUrl.value = "";
|
|
||||||
settingKavitaUser.value = "";
|
|
||||||
settingKavitaPass.value = "";
|
|
||||||
settingGotifyUrl.value = "";
|
|
||||||
settingGotifyAppToken.value = "";
|
|
||||||
settingLunaseaWebhook.value = "";
|
|
||||||
settingNtfyAuth.value = "";
|
|
||||||
settingNtfyEndpoint.value = "";
|
|
||||||
settingApiUri.value = "";
|
|
||||||
settingMangaHoverCheckbox.checked = false;
|
|
||||||
|
|
||||||
GetSettings().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
settingDownloadLocation.innerText = json.downloadLocation;
|
|
||||||
settingApiUri.placeholder = apiUri;
|
|
||||||
//console.log(json.styleSheet);
|
|
||||||
if (json.styleSheet == 'hover') {
|
|
||||||
settingMangaHoverCheckbox.checked = true;
|
|
||||||
document.getElementById('pagestyle').setAttribute('href', 'styles/style_mangahover.css');
|
|
||||||
} else {
|
|
||||||
settingMangaHoverCheckbox.checked = false;
|
|
||||||
document.getElementById('pagestyle').setAttribute('href', 'styles/style_default.css');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
GetLibraryConnectors().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
json.forEach(connector => {
|
|
||||||
switch(libraryConnectorTypes[connector.libraryType]){
|
|
||||||
case "Kavita":
|
|
||||||
settingKavitaConfigured.innerText = "✅";
|
|
||||||
settingKavitaUrl.placeholder = connector.baseUrl;
|
|
||||||
settingKavitaUser.placeholder = "***";
|
|
||||||
settingKavitaPass.placeholder = "***";
|
|
||||||
break;
|
|
||||||
case "Komga":
|
|
||||||
settingKomgaConfigured.innerText = "✅";
|
|
||||||
settingKomgaUrl.placeholder = connector.baseUrl;
|
|
||||||
settingKomgaUser.placeholder = "***";
|
|
||||||
settingKomgaPass.placeholder = "***";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Unknown type");
|
|
||||||
console.log(connector);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
GetNotificationConnectors().then((json) => {
|
|
||||||
//console.log(json);
|
|
||||||
json.forEach(connector => {
|
|
||||||
switch(notificationConnectorTypes[connector.notificationConnectorType]){
|
|
||||||
case "Gotify":
|
|
||||||
settingGotifyUrl.placeholder = connector.endpoint;
|
|
||||||
settingGotifyAppToken.placeholder = "***";
|
|
||||||
settingGotifyConfigured.innerText = "✅";
|
|
||||||
break;
|
|
||||||
case "LunaSea":
|
|
||||||
settingLunaseaConfigured.innerText = "✅";
|
|
||||||
settingLunaseaWebhook.placeholder = connector.id;
|
|
||||||
break;
|
|
||||||
case "Ntfy":
|
|
||||||
settingNtfyConfigured.innerText = "✅";
|
|
||||||
settingNtfyEndpoint.placeholder = connector.endpoint;
|
|
||||||
settingNtfyAuth.placeholder = "***";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Unknown type");
|
|
||||||
console.log(connector);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenSettings();
|
|
||||||
Setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
function utf8_to_b64(str) {
|
|
||||||
return window.btoa(unescape(encodeURIComponent( str )));
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateJobs(){
|
|
||||||
|
|
||||||
GetMonitorJobs().then((json) => {
|
|
||||||
if(monitoringJobsCount != json.length){
|
|
||||||
ResetContent();
|
|
||||||
//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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//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 = "jobWrapper";
|
|
||||||
wrapper.id = GetValidSelector(jobjson.id);
|
|
||||||
|
|
||||||
var image = document.createElement("img");
|
|
||||||
image.className = "jobImage";
|
|
||||||
image.src = GetCoverUrl(manga.internalId);
|
|
||||||
wrapper.appendChild(image);
|
|
||||||
|
|
||||||
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;
|
|
||||||
wrapper.appendChild(title);
|
|
||||||
|
|
||||||
var progressBar = document.createElement("progress");
|
|
||||||
progressBar.className = "jobProgressBar";
|
|
||||||
progressBar.id = `jobProgressBar${GetValidSelector(jobjson.id)}`;
|
|
||||||
wrapper.appendChild(progressBar);
|
|
||||||
|
|
||||||
var progressSpan = document.createElement("span");
|
|
||||||
progressSpan.className = "jobProgressSpan";
|
|
||||||
progressSpan.id = `jobProgressSpan${GetValidSelector(jobjson.id)}`;
|
|
||||||
progressSpan.innerText = "0% 00:00:00";
|
|
||||||
wrapper.appendChild(progressSpan);
|
|
||||||
|
|
||||||
var cancelSpan = document.createElement("span");
|
|
||||||
cancelSpan.className = "jobCancel";
|
|
||||||
cancelSpan.innerText = "Cancel";
|
|
||||||
cancelSpan.addEventListener("click", () => CancelJob(jobjson.id));
|
|
||||||
wrapper.appendChild(cancelSpan);
|
|
||||||
|
|
||||||
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,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,696 +0,0 @@
|
|||||||
:root{
|
|
||||||
--background-color: #030304;
|
|
||||||
--second-background-color: #fff;
|
|
||||||
--primary-color: #f5a9b8;
|
|
||||||
--secondary-color: #5bcefa;
|
|
||||||
--accent-color: #fff;
|
|
||||||
--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%;
|
|
||||||
margin-right: 10px;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
spacer{
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchdiv{
|
|
||||||
display: block;
|
|
||||||
margin: 0 10px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#searchbox {
|
|
||||||
padding: 3px 10px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14pt;
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingscog {
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 0px 30px;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsPopup popup-content{
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: start;
|
|
||||||
margin: 15px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsPopup popup-content > * {
|
|
||||||
margin: 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsPopup popup-content .title {
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
#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 20px;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status {
|
|
||||||
display: inline-block;
|
|
||||||
height: 10px;
|
|
||||||
width: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin: 5px;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status:hover > status-tooltip {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
status-tooltip {
|
|
||||||
visibility: hidden;
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
/*Position*/
|
|
||||||
left: -50px ;
|
|
||||||
bottom: 150%;
|
|
||||||
z-index: 3;
|
|
||||||
|
|
||||||
/*Text Properties*/
|
|
||||||
font-size:10pt;
|
|
||||||
font-weight:bold;
|
|
||||||
color:white;
|
|
||||||
text-align: center;
|
|
||||||
padding: 5px 5px;
|
|
||||||
|
|
||||||
/*Size*/
|
|
||||||
width:100px;
|
|
||||||
background-color: black;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
status-tooltip::after {
|
|
||||||
content: " ";
|
|
||||||
position: absolute;
|
|
||||||
top: 100%; /* At the bottom of the tooltip */
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -5px;
|
|
||||||
border-width: 5px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: black transparent transparent transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication img {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
z-index: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup{
|
|
||||||
display: none;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 2;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
popup popup-window {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 3;
|
|
||||||
left: 25%;
|
|
||||||
top: 100px;
|
|
||||||
width: 50%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--second-background-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-title {
|
|
||||||
height: 30px;
|
|
||||||
font-size: 14pt;
|
|
||||||
font-weight: bolder;
|
|
||||||
padding: 5px 10px;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--accent-color)
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-content{
|
|
||||||
margin: 15px 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-content div > * {
|
|
||||||
margin: 2px 3px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-content input, select {
|
|
||||||
padding: 3px 4px;
|
|
||||||
width: 130px;
|
|
||||||
border: 1px solid lightgrey;
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#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: rgba(245, 169, 184, 0.58);
|
|
||||||
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); }
|
|
||||||
}
|
|
||||||
|
|
||||||
#jobStatusView {
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
#jobStatusView > popup-window {
|
|
||||||
top: 80px;
|
|
||||||
width: 50%;
|
|
||||||
max-height: calc(100% - 140px);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
#jobStatusView > popup-window > div {
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
width: 50%;
|
|
||||||
margin: 0;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#jobStatusView > popup-window > div > div {
|
|
||||||
overflow-x: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper {
|
|
||||||
width: 90%;
|
|
||||||
margin: 2px 5%;
|
|
||||||
height: 100px;
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background-color: rgba(187,187,187,0.4);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper > .jobImage {
|
|
||||||
height: 90%;
|
|
||||||
width: 20%;
|
|
||||||
left: 5px;
|
|
||||||
object-fit: contain;
|
|
||||||
position: absolute;
|
|
||||||
top: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper > .jobTitle {
|
|
||||||
position: absolute;
|
|
||||||
left: calc(20% + 10px);
|
|
||||||
top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper > .jobProgressBar {
|
|
||||||
position: absolute;
|
|
||||||
left: calc(20% + 10px);
|
|
||||||
bottom: calc(12pt + 10px);
|
|
||||||
width: calc(80% - 20px);
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper > .jobProgressSpan {
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
bottom: calc(12pt + 20px);
|
|
||||||
width: 60%;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper > .jobCancel {
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
bottom: 5px;
|
|
||||||
font-size: 12pt;
|
|
||||||
color: var(--secondary-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
@ -1,709 +0,0 @@
|
|||||||
:root{
|
|
||||||
--background-color: #030304;
|
|
||||||
--second-background-color: #fff;
|
|
||||||
--primary-color: #f5a9b8;
|
|
||||||
--secondary-color: #5bcefa;
|
|
||||||
--accent-color: #fff;
|
|
||||||
--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%;
|
|
||||||
margin-right: 10px;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
spacer{
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchdiv{
|
|
||||||
display: block;
|
|
||||||
margin: 0 10px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#searchbox {
|
|
||||||
padding: 3px 10px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14pt;
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingscog {
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 0px 30px;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsPopup popup-content{
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: start;
|
|
||||||
margin: 15px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsPopup popup-content > * {
|
|
||||||
margin: 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsPopup popup-content .title {
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
#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 20px;
|
|
||||||
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%;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-information {
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-information * {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status {
|
|
||||||
display: inline-block;
|
|
||||||
height: 10px;
|
|
||||||
width: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin: 5px;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
publication-status:hover > status-tooltip {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
status-tooltip {
|
|
||||||
visibility: hidden;
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
/*Position*/
|
|
||||||
left: -50px ;
|
|
||||||
bottom: 150%;
|
|
||||||
z-index: 3;
|
|
||||||
|
|
||||||
/*Text Properties*/
|
|
||||||
font-size:10pt;
|
|
||||||
font-weight:bold;
|
|
||||||
color:white;
|
|
||||||
text-align: center;
|
|
||||||
padding: 5px 5px;
|
|
||||||
|
|
||||||
/*Size*/
|
|
||||||
width:100px;
|
|
||||||
background-color: black;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
status-tooltip::after {
|
|
||||||
content: " ";
|
|
||||||
position: absolute;
|
|
||||||
top: 100%; /* At the bottom of the tooltip */
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -5px;
|
|
||||||
border-width: 5px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: black transparent transparent transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup{
|
|
||||||
display: none;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 2;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
popup popup-window {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 3;
|
|
||||||
left: 25%;
|
|
||||||
top: 100px;
|
|
||||||
width: 50%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--second-background-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-title {
|
|
||||||
height: 30px;
|
|
||||||
font-size: 14pt;
|
|
||||||
font-weight: bolder;
|
|
||||||
padding: 5px 10px;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--accent-color)
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-content{
|
|
||||||
margin: 15px 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-content div > * {
|
|
||||||
margin: 2px 3px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
popup popup-window popup-content input, select {
|
|
||||||
padding: 3px 4px;
|
|
||||||
width: 130px;
|
|
||||||
border: 1px solid lightgrey;
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#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: rgba(245, 169, 184, 0.58);
|
|
||||||
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); }
|
|
||||||
}
|
|
||||||
|
|
||||||
#jobStatusView {
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
#jobStatusView > popup-window {
|
|
||||||
top: 80px;
|
|
||||||
width: 50%;
|
|
||||||
max-height: calc(100% - 140px);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
#jobStatusView > popup-window > div {
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
width: 50%;
|
|
||||||
margin: 0;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#jobStatusView > popup-window > div > div {
|
|
||||||
overflow-x: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper {
|
|
||||||
width: 90%;
|
|
||||||
margin: 2px 5%;
|
|
||||||
height: 100px;
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background-color: rgba(187,187,187,0.4);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper > .jobImage {
|
|
||||||
height: 90%;
|
|
||||||
width: 20%;
|
|
||||||
left: 5px;
|
|
||||||
object-fit: contain;
|
|
||||||
position: absolute;
|
|
||||||
top: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper > .jobTitle {
|
|
||||||
position: absolute;
|
|
||||||
left: calc(20% + 10px);
|
|
||||||
top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper > .jobProgressBar {
|
|
||||||
position: absolute;
|
|
||||||
left: calc(20% + 10px);
|
|
||||||
bottom: calc(12pt + 10px);
|
|
||||||
width: calc(80% - 20px);
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper > .jobProgressSpan {
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
bottom: calc(12pt + 20px);
|
|
||||||
width: 60%;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jobWrapper > .jobCancel {
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
bottom: 5px;
|
|
||||||
font-size: 12pt;
|
|
||||||
color: var(--secondary-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
|
||||||
tranga-api:
|
|
||||||
image: glax/tranga-api:cuttingedge
|
|
||||||
container_name: tranga-api
|
|
||||||
volumes:
|
|
||||||
- ./tranga:/usr/share/Tranga-API #1 when replacing ./tranga replace #2 with same value
|
|
||||||
- ./Manga:/Manga
|
|
||||||
ports:
|
|
||||||
- "6531:6531"
|
|
||||||
restart: unless-stopped
|
|
||||||
tranga-website:
|
|
||||||
image: glax/tranga-website:cuttingedge
|
|
||||||
container_name: tranga-website
|
|
||||||
volumes:
|
|
||||||
- ./tranga/imageCache:/usr/share/nginx/html/imageCache:ro #2 when replacing Point to same value as #1/imageCache
|
|
||||||
ports:
|
|
||||||
- "9555:80"
|
|
||||||
depends_on:
|
|
||||||
- tranga-api
|
|
||||||
restart: unless-stopped
|
|
26
nginx/fastcgi.conf
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
|
fastcgi_param CONTENT_TYPE $content_type;
|
||||||
|
fastcgi_param CONTENT_LENGTH $content_length;
|
||||||
|
|
||||||
|
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||||
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
|
fastcgi_param DOCUMENT_URI $document_uri;
|
||||||
|
fastcgi_param DOCUMENT_ROOT $document_root;
|
||||||
|
fastcgi_param SERVER_PROTOCOL $server_protocol;
|
||||||
|
fastcgi_param REQUEST_SCHEME $scheme;
|
||||||
|
fastcgi_param HTTPS $https if_not_empty;
|
||||||
|
|
||||||
|
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
|
||||||
|
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
|
||||||
|
|
||||||
|
fastcgi_param REMOTE_ADDR $remote_addr;
|
||||||
|
fastcgi_param REMOTE_PORT $remote_port;
|
||||||
|
fastcgi_param SERVER_ADDR $server_addr;
|
||||||
|
fastcgi_param SERVER_PORT $server_port;
|
||||||
|
fastcgi_param SERVER_NAME $server_name;
|
||||||
|
|
||||||
|
# PHP only, required if PHP was built with --enable-force-cgi-redirect
|
||||||
|
fastcgi_param REDIRECT_STATUS 200;
|
25
nginx/fastcgi_params
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
|
fastcgi_param CONTENT_TYPE $content_type;
|
||||||
|
fastcgi_param CONTENT_LENGTH $content_length;
|
||||||
|
|
||||||
|
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||||
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
|
fastcgi_param DOCUMENT_URI $document_uri;
|
||||||
|
fastcgi_param DOCUMENT_ROOT $document_root;
|
||||||
|
fastcgi_param SERVER_PROTOCOL $server_protocol;
|
||||||
|
fastcgi_param REQUEST_SCHEME $scheme;
|
||||||
|
fastcgi_param HTTPS $https if_not_empty;
|
||||||
|
|
||||||
|
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
|
||||||
|
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
|
||||||
|
|
||||||
|
fastcgi_param REMOTE_ADDR $remote_addr;
|
||||||
|
fastcgi_param REMOTE_PORT $remote_port;
|
||||||
|
fastcgi_param SERVER_ADDR $server_addr;
|
||||||
|
fastcgi_param SERVER_PORT $server_port;
|
||||||
|
fastcgi_param SERVER_NAME $server_name;
|
||||||
|
|
||||||
|
# PHP only, required if PHP was built with --enable-force-cgi-redirect
|
||||||
|
fastcgi_param REDIRECT_STATUS 200
|
99
nginx/mimes.types
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
|
||||||
|
types {
|
||||||
|
text/html html htm shtml;
|
||||||
|
text/css css;
|
||||||
|
text/xml xml;
|
||||||
|
image/gif gif;
|
||||||
|
image/jpeg jpeg jpg;
|
||||||
|
application/javascript js;
|
||||||
|
application/atom+xml atom;
|
||||||
|
application/rss+xml rss;
|
||||||
|
|
||||||
|
text/mathml mml;
|
||||||
|
text/plain txt;
|
||||||
|
text/vnd.sun.j2me.app-descriptor jad;
|
||||||
|
text/vnd.wap.wml wml;
|
||||||
|
text/x-component htc;
|
||||||
|
|
||||||
|
image/avif avif;
|
||||||
|
image/png png;
|
||||||
|
image/svg+xml svg svgz;
|
||||||
|
image/tiff tif tiff;
|
||||||
|
image/vnd.wap.wbmp wbmp;
|
||||||
|
image/webp webp;
|
||||||
|
image/x-icon ico;
|
||||||
|
image/x-jng jng;
|
||||||
|
image/x-ms-bmp bmp;
|
||||||
|
|
||||||
|
font/woff woff;
|
||||||
|
font/woff2 woff2;
|
||||||
|
|
||||||
|
application/java-archive jar war ear;
|
||||||
|
application/json json;
|
||||||
|
application/mac-binhex40 hqx;
|
||||||
|
application/msword doc;
|
||||||
|
application/pdf pdf;
|
||||||
|
application/postscript ps eps ai;
|
||||||
|
application/rtf rtf;
|
||||||
|
application/vnd.apple.mpegurl m3u8;
|
||||||
|
application/vnd.google-earth.kml+xml kml;
|
||||||
|
application/vnd.google-earth.kmz kmz;
|
||||||
|
application/vnd.ms-excel xls;
|
||||||
|
application/vnd.ms-fontobject eot;
|
||||||
|
application/vnd.ms-powerpoint ppt;
|
||||||
|
application/vnd.oasis.opendocument.graphics odg;
|
||||||
|
application/vnd.oasis.opendocument.presentation odp;
|
||||||
|
application/vnd.oasis.opendocument.spreadsheet ods;
|
||||||
|
application/vnd.oasis.opendocument.text odt;
|
||||||
|
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||||
|
pptx;
|
||||||
|
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||||
|
xlsx;
|
||||||
|
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||||
|
docx;
|
||||||
|
application/vnd.wap.wmlc wmlc;
|
||||||
|
application/wasm wasm;
|
||||||
|
application/x-7z-compressed 7z;
|
||||||
|
application/x-cocoa cco;
|
||||||
|
application/x-java-archive-diff jardiff;
|
||||||
|
application/x-java-jnlp-file jnlp;
|
||||||
|
application/x-makeself run;
|
||||||
|
application/x-perl pl pm;
|
||||||
|
application/x-pilot prc pdb;
|
||||||
|
application/x-rar-compressed rar;
|
||||||
|
application/x-redhat-package-manager rpm;
|
||||||
|
application/x-sea sea;
|
||||||
|
application/x-shockwave-flash swf;
|
||||||
|
application/x-stuffit sit;
|
||||||
|
application/x-tcl tcl tk;
|
||||||
|
application/x-x509-ca-cert der pem crt;
|
||||||
|
application/x-xpinstall xpi;
|
||||||
|
application/xhtml+xml xhtml;
|
||||||
|
application/xspf+xml xspf;
|
||||||
|
application/zip zip;
|
||||||
|
|
||||||
|
application/octet-stream bin exe dll;
|
||||||
|
application/octet-stream deb;
|
||||||
|
application/octet-stream dmg;
|
||||||
|
application/octet-stream iso img;
|
||||||
|
application/octet-stream msi msp msm;
|
||||||
|
|
||||||
|
audio/midi mid midi kar;
|
||||||
|
audio/mpeg mp3;
|
||||||
|
audio/ogg ogg;
|
||||||
|
audio/x-m4a m4a;
|
||||||
|
audio/x-realaudio ra;
|
||||||
|
|
||||||
|
video/3gpp 3gpp 3gp;
|
||||||
|
video/mp2t ts;
|
||||||
|
video/mp4 mp4;
|
||||||
|
video/mpeg mpeg mpg;
|
||||||
|
video/quicktime mov;
|
||||||
|
video/webm webm;
|
||||||
|
video/x-flv flv;
|
||||||
|
video/x-m4v m4v;
|
||||||
|
video/x-mng mng;
|
||||||
|
video/x-ms-asf asx asf;
|
||||||
|
video/x-ms-wmv wmv;
|
||||||
|
video/x-msvideo avi;
|
||||||
|
}
|
32
nginx/nginx.conf
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log notice;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
#tcp_nopush on;
|
||||||
|
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
#gzip on;
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
17
nginx/scgi_params
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
scgi_param REQUEST_METHOD $request_method;
|
||||||
|
scgi_param REQUEST_URI $request_uri;
|
||||||
|
scgi_param QUERY_STRING $query_string;
|
||||||
|
scgi_param CONTENT_TYPE $content_type;
|
||||||
|
|
||||||
|
scgi_param DOCUMENT_URI $document_uri;
|
||||||
|
scgi_param DOCUMENT_ROOT $document_root;
|
||||||
|
scgi_param SCGI 1;
|
||||||
|
scgi_param SERVER_PROTOCOL $server_protocol;
|
||||||
|
scgi_param REQUEST_SCHEME $scheme;
|
||||||
|
scgi_param HTTPS $https if_not_empty;
|
||||||
|
|
||||||
|
scgi_param REMOTE_ADDR $remote_addr;
|
||||||
|
scgi_param REMOTE_PORT $remote_port;
|
||||||
|
scgi_param SERVER_PORT $server_port;
|
||||||
|
scgi_param SERVER_NAME $server_name;
|
29
nginx/templates/default.conf.template
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
access_log /dev/stdout main;
|
||||||
|
error_log /dev/stderr;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
rewrite ^/api/(.*)$ /$1 break;
|
||||||
|
proxy_pass $API_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
}
|
||||||
|
|
||||||
|
# redirect server error pages to the static page /50x.html
|
||||||
|
#
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
17
nginx/uwsgi_params
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
uwsgi_param QUERY_STRING $query_string;
|
||||||
|
uwsgi_param REQUEST_METHOD $request_method;
|
||||||
|
uwsgi_param CONTENT_TYPE $content_type;
|
||||||
|
uwsgi_param CONTENT_LENGTH $content_length;
|
||||||
|
|
||||||
|
uwsgi_param REQUEST_URI $request_uri;
|
||||||
|
uwsgi_param PATH_INFO $document_uri;
|
||||||
|
uwsgi_param DOCUMENT_ROOT $document_root;
|
||||||
|
uwsgi_param SERVER_PROTOCOL $server_protocol;
|
||||||
|
uwsgi_param REQUEST_SCHEME $scheme;
|
||||||
|
uwsgi_param HTTPS $https if_not_empty;
|
||||||
|
|
||||||
|
uwsgi_param REMOTE_ADDR $remote_addr;
|
||||||
|
uwsgi_param REMOTE_PORT $remote_port;
|
||||||
|
uwsgi_param SERVER_PORT $server_port;
|
||||||
|
uwsgi_param SERVER_NAME $server_name;
|
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>
|
6394
tranga-website/package-lock.json
generated
Normal file
35
tranga-website/package.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"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;
|
||||||
|
}
|
107
tranga-website/src/App.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
import './App.css'
|
||||||
|
import Settings from "./Settings.tsx";
|
||||||
|
import Header from "./Header.tsx";
|
||||||
|
import {Badge, Button} from "@mui/joy";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {ApiUriContext} from "./api/fetchApi.tsx";
|
||||||
|
import Search from './Components/Search.tsx';
|
||||||
|
import MangaList from "./Components/MangaList.tsx";
|
||||||
|
import {MangaConnectorContext} from "./api/Contexts/MangaConnectorContext.tsx";
|
||||||
|
import IMangaConnector from "./api/types/IMangaConnector.ts";
|
||||||
|
import {GetAllConnectors} from "./api/MangaConnector.tsx";
|
||||||
|
import JobsDrawer from "./Components/Jobs.tsx";
|
||||||
|
import {MangaContext} from "./api/Contexts/MangaContext.tsx";
|
||||||
|
import IManga from "./api/types/IManga.ts";
|
||||||
|
import {GetMangaById} from "./api/Manga.tsx";
|
||||||
|
import IChapter from "./api/types/IChapter.ts";
|
||||||
|
import {GetChapterFromId} from "./api/Chapter.tsx";
|
||||||
|
import {ChapterContext} from "./api/Contexts/ChapterContext.tsx";
|
||||||
|
|
||||||
|
export default function App () {
|
||||||
|
|
||||||
|
const [showSettings, setShowSettings] = useState<boolean>(false);
|
||||||
|
const [showSearch, setShowSearch] = useState<boolean>(false);
|
||||||
|
const [showJobs, setShowJobs] = useState<boolean>(false);
|
||||||
|
const [apiConnected, setApiConnected] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/api";
|
||||||
|
|
||||||
|
const [apiUri, setApiUri] = useState<string>(apiUriStr);
|
||||||
|
const [mangas, setMangas] = useState<IManga[]>([]);
|
||||||
|
const [chapters, setChapters] = useState<IChapter[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("apiUri", apiUri);
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
const [mangaPromises, setMangaPromises] = useState(new Map<string, Promise<IManga | undefined>>());
|
||||||
|
const GetManga = (mangaId: string) : Promise<IManga | undefined> => {
|
||||||
|
const promise = mangaPromises.get(mangaId);
|
||||||
|
if(promise) return promise;
|
||||||
|
const p = new Promise<IManga | undefined>((resolve, reject) => {
|
||||||
|
let ret = mangas?.find(m => m.mangaId == mangaId);
|
||||||
|
if (ret) resolve(ret);
|
||||||
|
|
||||||
|
console.log(`Fetching manga ${mangaId}`);
|
||||||
|
GetMangaById(apiUri, mangaId).then(manga => {
|
||||||
|
if(manga && mangas?.find(m => m.mangaId == mangaId) === undefined)
|
||||||
|
setMangas([...mangas, manga]);
|
||||||
|
resolve(manga);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
setMangaPromises(mangaPromises.set(mangaId, p));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [chapterPromises, setChapterPromises] = useState(new Map<string, Promise<IChapter | undefined>>());
|
||||||
|
const GetChapter = (chapterId: string) : Promise<IChapter | undefined> => {
|
||||||
|
const promise = chapterPromises.get(chapterId);
|
||||||
|
if(promise) return promise;
|
||||||
|
const p = new Promise<IChapter | undefined>((resolve, reject) => {
|
||||||
|
let ret = chapters?.find(c => c.chapterId == chapterId);
|
||||||
|
if (ret) resolve(ret);
|
||||||
|
|
||||||
|
console.log(`Fetching chapter ${chapterId}`);
|
||||||
|
GetChapterFromId(apiUri, chapterId).then(chapter => {
|
||||||
|
if(chapter && chapters?.find(c => c.chapterId == chapterId) === undefined)
|
||||||
|
setChapters([...chapters, chapter]);
|
||||||
|
resolve(chapter);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
setChapterPromises(chapterPromises.set(chapterId, p));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!apiConnected) return;
|
||||||
|
GetAllConnectors(apiUri).then(setMangaConnectors);
|
||||||
|
}, [apiConnected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ApiUriContext.Provider value={apiUri}>
|
||||||
|
<MangaConnectorContext.Provider value={mangaConnectors}>
|
||||||
|
<MangaContext.Provider value={{mangas, GetManga}}>
|
||||||
|
<ChapterContext.Provider value={{chapters, GetChapter}}>
|
||||||
|
<Sheet className={"app"}>
|
||||||
|
<Header>
|
||||||
|
<Badge color={"danger"} invisible={apiConnected} badgeContent={"!"}>
|
||||||
|
<Button onClick={() => setShowSettings(true)}>Settings</Button>
|
||||||
|
</Badge>
|
||||||
|
<Button onClick={() => setShowJobs(true)}>Jobs</Button>
|
||||||
|
</Header>
|
||||||
|
<Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri} setConnected={setApiConnected} />
|
||||||
|
<Search open={showSearch} setOpen={setShowSearch} />
|
||||||
|
<JobsDrawer open={showJobs} connected={apiConnected} setOpen={setShowJobs} />
|
||||||
|
<Sheet className={"app-content"}>
|
||||||
|
<MangaList connected={apiConnected} setShowSearch={setShowSearch} />
|
||||||
|
</Sheet>
|
||||||
|
</Sheet>
|
||||||
|
</ChapterContext.Provider>
|
||||||
|
</MangaContext.Provider>
|
||||||
|
</MangaConnectorContext.Provider>
|
||||||
|
</ApiUriContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
50
tranga-website/src/Components/Chapter.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React, {ReactElement, useContext, useState} from "react";
|
||||||
|
import IChapter from "../api/types/IChapter.ts";
|
||||||
|
import {Box, Chip, Link, Stack, Tooltip, Typography} from "@mui/joy";
|
||||||
|
import {MangaFromId} from "./Manga.tsx";
|
||||||
|
import {ChapterContext} from "../api/Contexts/ChapterContext.tsx";
|
||||||
|
import Drawer from "@mui/joy/Drawer";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
import {Archive} from "@mui/icons-material";
|
||||||
|
|
||||||
|
export function ChapterPopupFromId({chapterId, open, setOpen, children}: { chapterId: string | null, open: boolean, setOpen: React.Dispatch<React.SetStateAction<boolean>>, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }) {
|
||||||
|
return (
|
||||||
|
<Drawer anchor={"bottom"} open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalClose />
|
||||||
|
{
|
||||||
|
chapterId !== null ?
|
||||||
|
<ChapterFromId chapterId={chapterId}>{children}</ChapterFromId>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChapterFromId({chapterId, children} : { chapterId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
|
||||||
|
const chapterContext = useContext(ChapterContext);
|
||||||
|
|
||||||
|
const [chapter, setChapter] = useState<IChapter | undefined>(undefined);
|
||||||
|
chapterContext.GetChapter(chapterId).then(setChapter);
|
||||||
|
|
||||||
|
return (
|
||||||
|
chapter === undefined ?
|
||||||
|
null
|
||||||
|
:
|
||||||
|
<Chapter chapter={chapter}>{children}</Chapter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Chapter({chapter, children} : { chapter: IChapter, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
|
||||||
|
return (
|
||||||
|
<Stack direction={"row"} spacing={5} sx={{paddingTop: "10px"}}>
|
||||||
|
<MangaFromId mangaId={chapter.parentMangaId} />
|
||||||
|
<Box>
|
||||||
|
<Link target={"_blank"} level={"title-lg"} href={chapter.url}>{chapter.title}</Link>
|
||||||
|
<Typography>Volume <Chip>{chapter.volumeNumber}</Chip></Typography>
|
||||||
|
<Typography>Chapter <Chip>{chapter.chapterNumber}</Chip></Typography>
|
||||||
|
<Tooltip title={chapter.fullArchiveFilePath} placement={"bottom-start"}><Archive /></Tooltip>
|
||||||
|
</Box>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
163
tranga-website/src/Components/Jobs.tsx
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DialogContent, DialogTitle,
|
||||||
|
Drawer,
|
||||||
|
Input,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Typography
|
||||||
|
} from "@mui/joy";
|
||||||
|
import {GetJobsInState, GetJobsOfTypeAndWithState, GetJobsWithType, StartJob} from "../api/Job.tsx";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import {ApiUriContext} from "../api/fetchApi.tsx";
|
||||||
|
import IJob, {JobState, JobStateToString, JobType, JobTypeToString} from "../api/types/Jobs/IJob.ts";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
import {MangaPopupFromId} from "./MangaPopup.tsx";
|
||||||
|
import IJobWithMangaId from "../api/types/Jobs/IJobWithMangaId.ts";
|
||||||
|
import {ChapterPopupFromId} from "./Chapter.tsx";
|
||||||
|
import IJobWithChapterId from "../api/types/Jobs/IJobWithChapterId.tsx";
|
||||||
|
|
||||||
|
export default function JobsDrawer({open, connected, setOpen} : {open: boolean, connected: boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>}) {
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
|
const [allJobs, setAllJobs] = useState<IJob[]>([]);
|
||||||
|
|
||||||
|
const [filterState, setFilterState] = useState<string|null>(null);
|
||||||
|
const [filterType, setFilterType] = useState<string|null>(null);
|
||||||
|
|
||||||
|
const pageSize = 10;
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
const updateDisplayJobs = useCallback(() => {
|
||||||
|
if(!connected)
|
||||||
|
return;
|
||||||
|
if (filterState === null && filterType === null)
|
||||||
|
setAllJobs([]);
|
||||||
|
else if (filterState === null && filterType != null)
|
||||||
|
GetJobsWithType(apiUri, filterType as unknown as JobType).then(setAllJobs);
|
||||||
|
else if (filterState != null && filterType === null)
|
||||||
|
GetJobsInState(apiUri, filterState as unknown as JobState).then(setAllJobs);
|
||||||
|
else if (filterState != null && filterType != null)
|
||||||
|
GetJobsOfTypeAndWithState(apiUri, filterType as unknown as JobType, filterState as unknown as JobState).then(setAllJobs);
|
||||||
|
}, [connected, filterType, filterState]);
|
||||||
|
|
||||||
|
const timerRef = React.useRef<ReturnType<typeof setInterval>>(undefined);
|
||||||
|
useEffect(() => {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
updateDisplayJobs();
|
||||||
|
timerRef.current = setInterval(updateDisplayJobs, 2000);
|
||||||
|
}, [filterState, filterType]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !connected)
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
}, [open, connected]);
|
||||||
|
|
||||||
|
const handleChangeState = (
|
||||||
|
_: React.SyntheticEvent | null,
|
||||||
|
newValue: string | null,
|
||||||
|
) => {
|
||||||
|
setFilterState(newValue);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeType = (
|
||||||
|
_: React.SyntheticEvent | null,
|
||||||
|
newValue: string | null,
|
||||||
|
) => {
|
||||||
|
setFilterType(newValue);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [mangaPopupOpen, setMangaPopupOpen] = React.useState(false);
|
||||||
|
const [selectedMangaId, setSelectedMangaId] = useState<string | null>(null);
|
||||||
|
const OpenMangaPopupDrawer = (mangaId: string) => {
|
||||||
|
setSelectedMangaId(mangaId);
|
||||||
|
setMangaPopupOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [chapterPopupOpen, setChapterPopupOpen] = React.useState(false);
|
||||||
|
const [selectedChapterId, setSelectedChapterId] = React.useState<string | null>(null);
|
||||||
|
const OpenChapterPopupDrawer = (chapterId: string) => {
|
||||||
|
setSelectedChapterId(chapterId);
|
||||||
|
setChapterPopupOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReRunJob = useCallback((jobId: string) => {
|
||||||
|
StartJob(apiUri, jobId, false);
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer size={"lg"} anchor={"left"} open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalClose />
|
||||||
|
<DialogTitle><Typography level={"h2"}>Jobs</Typography></DialogTitle>
|
||||||
|
<Stack direction={"row"} spacing={2}>
|
||||||
|
<Select placeholder={"State"} value={filterState} onChange={handleChangeState} startDecorator={
|
||||||
|
<Typography>State</Typography>
|
||||||
|
}>
|
||||||
|
<Option value={null}>None</Option>
|
||||||
|
{Object.keys(JobState).map((state) => <Option value={state}>{JobStateToString(state)}</Option>)}
|
||||||
|
</Select>
|
||||||
|
<Select placeholder={"Type"} value={filterType} onChange={handleChangeType} startDecorator={
|
||||||
|
<Typography>Type</Typography>
|
||||||
|
}>
|
||||||
|
<Option value={null}>None</Option>
|
||||||
|
{Object.keys(JobType).map((type) => <Option value={type}>{JobTypeToString(type)}</Option>)}
|
||||||
|
</Select>
|
||||||
|
<Input type={"number"}
|
||||||
|
value={page}
|
||||||
|
onChange={(e) => setPage(parseInt(e.target.value))}
|
||||||
|
slotProps={{input: { min: 1, max: Math.ceil(allJobs.length / pageSize)}}}
|
||||||
|
startDecorator={<Typography>Page</Typography>}
|
||||||
|
endDecorator={<Typography>/{Math.ceil(allJobs.length / pageSize)}</Typography>}/>
|
||||||
|
</Stack>
|
||||||
|
<DialogContent>
|
||||||
|
<Table borderAxis={"bothBetween"} stickyHeader sx={{tableLayout: "auto", width: "100%"}}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Last Execution</th>
|
||||||
|
<th>Next Execution</th>
|
||||||
|
<th></th>
|
||||||
|
<th>Extra</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{allJobs.slice((page-1)*pageSize, page*pageSize).map((job) => (
|
||||||
|
<tr key={job.jobId}>
|
||||||
|
<td>{JobTypeToString(job.jobType)}</td>
|
||||||
|
<td>{JobStateToString(job.state)}</td>
|
||||||
|
<td>{new Date(job.lastExecution).toLocaleString()}</td>
|
||||||
|
<td>{new Date(job.nextExecution).toLocaleString()}</td>
|
||||||
|
<td style={{whiteSpace: "nowrap"}}><Button onClick={() => ReRunJob(job.jobId)}>Re-Run</Button></td>
|
||||||
|
<td>{ExtraContent(job, OpenMangaPopupDrawer, OpenChapterPopupDrawer)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</DialogContent>
|
||||||
|
<MangaPopupFromId mangaId={selectedMangaId} open={mangaPopupOpen} setOpen={setMangaPopupOpen} />
|
||||||
|
<ChapterPopupFromId chapterId={selectedChapterId} open={chapterPopupOpen} setOpen={setChapterPopupOpen} />
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExtraContent(job: IJob, OpenMangaPopupDrawer: (mangaId: string) => void, OpenChapterPopupDrawer: (IJobWithChapterId: string) => void){
|
||||||
|
switch(job.jobType){
|
||||||
|
case JobType.DownloadAvailableChaptersJob:
|
||||||
|
case JobType.DownloadMangaCoverJob:
|
||||||
|
case JobType.RetrieveChaptersJob:
|
||||||
|
case JobType.UpdateChaptersDownloadedJob:
|
||||||
|
case JobType.UpdateCoverJob:
|
||||||
|
case JobType.MoveMangaLibraryJob:
|
||||||
|
return <Button onClick={() => OpenMangaPopupDrawer((job as IJobWithMangaId).mangaId)}>Open Manga</Button>
|
||||||
|
case JobType.DownloadSingleChapterJob:
|
||||||
|
return <Button onClick={() => OpenChapterPopupDrawer((job as IJobWithChapterId).chapterId)}>Show Chapter</Button>
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
119
tranga-website/src/Components/Manga.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import {Badge, Box, Card, CardContent, CardCover, Skeleton, Tooltip, Typography,} from "@mui/joy";
|
||||||
|
import IManga from "../api/types/IManga.ts";
|
||||||
|
import {CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
|
||||||
|
import {GetMangaCoverImageUrl} from "../api/Manga.tsx";
|
||||||
|
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
|
||||||
|
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
|
||||||
|
import {SxProps} from "@mui/joy/styles/types";
|
||||||
|
import MangaPopup from "./MangaPopup.tsx";
|
||||||
|
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx";
|
||||||
|
import {MangaContext} from "../api/Contexts/MangaContext.tsx";
|
||||||
|
|
||||||
|
export const CardWidth = 190;
|
||||||
|
export const CardHeight = 300;
|
||||||
|
|
||||||
|
const coverSx : SxProps = {
|
||||||
|
height: CardHeight + "px",
|
||||||
|
width: CardWidth + "px",
|
||||||
|
position: "relative",
|
||||||
|
}
|
||||||
|
|
||||||
|
const coverCss : CSSProperties = {
|
||||||
|
maxHeight: "calc("+CardHeight+"px + 2rem)",
|
||||||
|
maxWidth: "calc("+CardWidth+"px + 2rem)",
|
||||||
|
objectFit: "cover",
|
||||||
|
width: "calc("+CardHeight+"px + 2rem)",
|
||||||
|
height: "calc("+CardHeight+"px + 2rem)",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MangaFromId({mangaId, children} : { mangaId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
|
||||||
|
const mangaContext = useContext(MangaContext);
|
||||||
|
|
||||||
|
const [manga, setManga] = useState<IManga | undefined>(undefined);
|
||||||
|
mangaContext.GetManga(mangaId).then(setManga);
|
||||||
|
|
||||||
|
return (
|
||||||
|
manga === undefined ?
|
||||||
|
<Badge sx={{margin:"8px !important"}} badgeContent={<Skeleton><Tooltip title={"Loading"}><img width={"24pt"} height={"24pt"} src={"/blahaj.png"} /></Tooltip></Skeleton>} color={ReleaseStatusToPalette(MangaReleaseStatus.Completed)} size={"lg"}>
|
||||||
|
<Card sx={{height:"fit-content",width:"fit-content"}}>
|
||||||
|
<CardCover>
|
||||||
|
<img loading={"lazy"} style={coverCss} src={"/blahaj.png"} alt="Manga Cover"/>
|
||||||
|
</CardCover>
|
||||||
|
<CardCover sx={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(to bottom, rgba(0,0,0,0.4), rgba(0,0,0,0) 200px), linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0) 300px)',
|
||||||
|
}}/>
|
||||||
|
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
|
||||||
|
<Box sx={coverSx}>
|
||||||
|
<Typography level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
|
||||||
|
<Skeleton loading={true} animation={"wave"}>
|
||||||
|
{mangaId.split("").splice(0,mangaId.length/2).join(" ")}
|
||||||
|
</Skeleton>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Badge>
|
||||||
|
:
|
||||||
|
<Manga manga={manga} children={children} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Manga({manga: manga, children} : { manga: IManga, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
|
||||||
|
const CoverRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
const mangaConnector = useContext(MangaConnectorContext).find(all => all.name == manga.mangaConnectorName);
|
||||||
|
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
LoadMangaCover();
|
||||||
|
}, [manga, apiUri]);
|
||||||
|
|
||||||
|
const LoadMangaCover = useCallback(() => {
|
||||||
|
if(CoverRef.current == null)
|
||||||
|
return;
|
||||||
|
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
|
||||||
|
if(CoverRef.current.src == coverUrl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//Check if we can fetch the image exists (by fetching it), only then update
|
||||||
|
getData(coverUrl).then(() => {
|
||||||
|
if(CoverRef.current) CoverRef.current.src = coverUrl;
|
||||||
|
});
|
||||||
|
}, [manga, apiUri]);
|
||||||
|
|
||||||
|
const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"];
|
||||||
|
|
||||||
|
const maxLength = 50;
|
||||||
|
const mangaName = manga.name.length > maxLength ? manga.name.substring(0, maxLength-3) + "..." : manga.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge sx={{margin:"8px !important"}} badgeContent={mangaConnector ? <Tooltip color={ReleaseStatusToPalette(manga.releaseStatus)} title={manga.releaseStatus}><img width={"24pt"} height={"24pt"} src={mangaConnector.iconUrl} /></Tooltip> : manga.mangaConnectorName} color={ReleaseStatusToPalette(manga.releaseStatus)} size={"lg"}>
|
||||||
|
<Card sx={{height:"fit-content",width:"fit-content"}} onClick={(e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if(interactiveElements.find(x => x == target.localName) == undefined)
|
||||||
|
setExpanded(!expanded)}
|
||||||
|
}>
|
||||||
|
<CardCover>
|
||||||
|
<img loading={"lazy"} style={coverCss} src={GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current)} alt="Manga Cover"
|
||||||
|
ref={CoverRef}
|
||||||
|
onError={LoadMangaCover}/>
|
||||||
|
</CardCover>
|
||||||
|
<CardCover sx={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(to bottom, rgba(0,0,0,0.4), rgba(0,0,0,0) 200px), linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0) 300px)',
|
||||||
|
}}/>
|
||||||
|
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
|
||||||
|
<Box sx={coverSx}>
|
||||||
|
<Typography level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
|
||||||
|
{mangaName}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
<MangaPopup manga={manga} open={expanded} setOpen={setExpanded}>{children}</MangaPopup>
|
||||||
|
</Card>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
77
tranga-website/src/Components/MangaList.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import {Badge, Box, Button, Card, CardContent, CardCover, Stack, Tooltip, Typography} from "@mui/joy";
|
||||||
|
import {Dispatch, SetStateAction, useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import {ApiUriContext} from "../api/fetchApi.tsx";
|
||||||
|
import {DeleteJob, GetJobsWithType, StartJob} from "../api/Job.tsx";
|
||||||
|
import {JobType} from "../api/types/Jobs/IJob.ts";
|
||||||
|
import IDownloadAvailableChaptersJob from "../api/types/Jobs/IDownloadAvailableChaptersJob.ts";
|
||||||
|
import {CardHeight, CardWidth, MangaFromId} from "./Manga.tsx";
|
||||||
|
import {PlayArrow, Remove} from "@mui/icons-material";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export default function MangaList({connected, setShowSearch}: {connected: boolean, setShowSearch: Dispatch<SetStateAction<boolean>>} ) {
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
|
const [jobList, setJobList] = useState<IDownloadAvailableChaptersJob[]>([]);
|
||||||
|
|
||||||
|
const getJobList = useCallback(() => {
|
||||||
|
if(!connected)
|
||||||
|
return;
|
||||||
|
GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob).then((jl) => setJobList(jl as IDownloadAvailableChaptersJob[]));
|
||||||
|
},[apiUri,connected]);
|
||||||
|
|
||||||
|
const deleteJob = useCallback((jobId: string) => {
|
||||||
|
DeleteJob(apiUri, jobId).finally(() => getJobList());
|
||||||
|
},[apiUri]);
|
||||||
|
|
||||||
|
const startJob = useCallback((jobId: string) => {
|
||||||
|
StartJob(apiUri, jobId, true).finally(() => getJobList());
|
||||||
|
},[apiUri]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateTimer();
|
||||||
|
}, [connected, apiUri]);
|
||||||
|
|
||||||
|
const timerRef = React.useRef<ReturnType<typeof setInterval>>(undefined);
|
||||||
|
const updateTimer = useCallback(() => {
|
||||||
|
if(!connected){
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
return;
|
||||||
|
}else{
|
||||||
|
if(timerRef.current === undefined) {
|
||||||
|
console.log("Added timer!");
|
||||||
|
getJobList();
|
||||||
|
timerRef.current = setInterval(getJobList, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [getJobList, connected, timerRef]);
|
||||||
|
|
||||||
|
return(
|
||||||
|
<Stack direction="row" spacing={1} flexWrap={"wrap"} sx={{overflowX: 'hidden', overflowY: 'auto' /* Badge overflow */}} paddingTop={"6px" /* Badge overflow */}>
|
||||||
|
<Badge invisible sx={{margin: "8px !important"}}>
|
||||||
|
<Card onClick={() => setShowSearch(true)} sx={{height:"fit-content",width:"fit-content"}}>
|
||||||
|
<CardCover sx={{margin:"var(--Card-padding)"}}>
|
||||||
|
<img src={"/blahaj.png"} style={{height: CardHeight + "px", width: CardWidth + "px"}} />
|
||||||
|
</CardCover>
|
||||||
|
<CardCover sx={{
|
||||||
|
background: 'rgba(234, 119, 246, 0.14)',
|
||||||
|
backdropFilter: 'blur(6.9px)',
|
||||||
|
webkitBackdropFilter: 'blur(6.9px)',
|
||||||
|
}}/>
|
||||||
|
<CardContent>
|
||||||
|
<Box style={{height: CardHeight + "px", width: CardWidth + "px"}} >
|
||||||
|
<Typography level={"h1"}>Search</Typography>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Badge>
|
||||||
|
{jobList?.map((job) => (
|
||||||
|
<MangaFromId key={job.mangaId} mangaId={job.mangaId}>
|
||||||
|
<Tooltip title={"Last run: " + new Date(job.lastExecution).toLocaleString()}>
|
||||||
|
<Button color={"success"} endDecorator={<PlayArrow />} onClick={() => startJob(job.jobId)}>Start</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Button color={"danger"} endDecorator={<Remove />} onClick={() => deleteJob(job.jobId)}>Delete</Button>
|
||||||
|
</MangaFromId>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
186
tranga-website/src/Components/MangaPopup.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import IManga from "../api/types/IManga.ts";
|
||||||
|
import {Badge, Box, Chip, CircularProgress, Drawer, Input, Link, Skeleton, Stack, Typography} from "@mui/joy";
|
||||||
|
import React, {ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
|
||||||
|
import {
|
||||||
|
GetLatestChapterAvailable,
|
||||||
|
GetLatestChapterDownloaded,
|
||||||
|
GetMangaCoverImageUrl,
|
||||||
|
SetIgnoreThreshold
|
||||||
|
} from "../api/Manga.tsx";
|
||||||
|
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
|
||||||
|
import MarkdownPreview from "@uiw/react-markdown-preview";
|
||||||
|
import {CardHeight} from "./Manga.tsx";
|
||||||
|
import IChapter from "../api/types/IChapter.ts";
|
||||||
|
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
|
||||||
|
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx";
|
||||||
|
import {MangaContext} from "../api/Contexts/MangaContext.tsx";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
|
||||||
|
|
||||||
|
export function MangaPopupFromId({mangaId, open, setOpen, children} : {mangaId: string | null, open: boolean, setOpen: React.Dispatch<React.SetStateAction<boolean>>, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
|
||||||
|
const mangaContext = useContext(MangaContext);
|
||||||
|
|
||||||
|
const [manga, setManga] = useState<IManga | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || mangaId === null)
|
||||||
|
return;
|
||||||
|
mangaContext.GetManga(mangaId).then(setManga);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
manga === undefined ?
|
||||||
|
<Drawer anchor="bottom" size="lg" open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalClose />
|
||||||
|
<Stack direction="column" spacing={2} margin={"10px"}>
|
||||||
|
{ /* Cover and Description */ }
|
||||||
|
<Stack direction="row" spacing={2} margin={"10px"}>
|
||||||
|
<Badge sx={{margin:"8px !important"}} color={ReleaseStatusToPalette(MangaReleaseStatus.Unreleased)} size={"lg"}>
|
||||||
|
<img src="/blahaj.png" alt="Manga Cover"/>
|
||||||
|
</Badge>
|
||||||
|
<Box>
|
||||||
|
<Skeleton loading={true} animation={"wave"}>
|
||||||
|
{mangaId?.split("").splice(0,mangaId.length/2).join(" ")}
|
||||||
|
</Skeleton>
|
||||||
|
<Stack direction={"row"} flexWrap={"wrap"} useFlexGap={true} spacing={0.3} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
|
||||||
|
{mangaId?.split("").filter(x => Number.isNaN(x)).map(_ =>
|
||||||
|
<Skeleton loading={true} animation={"wave"}>
|
||||||
|
<Chip>Wow</Chip>
|
||||||
|
</Skeleton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<MarkdownPreview style={{backgroundColor: "transparent", color: "var(--joy-palette-neutral-50)", maxHeight:CardHeight*0.7+"px", overflowY:"auto", marginTop:"10px", scrollbarWidth: "thin"}} />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{ /* Actions */ }
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Drawer>
|
||||||
|
:
|
||||||
|
<MangaPopup manga={manga} open={open} setOpen={setOpen}>{children}</MangaPopup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MangaPopup({manga, open, setOpen, children} : {manga: IManga | null, open: boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
|
||||||
|
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
|
const CoverRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
const LoadMangaCover = useCallback(() => {
|
||||||
|
if(CoverRef.current == null || manga == null)
|
||||||
|
return;
|
||||||
|
if (!open)
|
||||||
|
return;
|
||||||
|
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
|
||||||
|
if(CoverRef.current.src == coverUrl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//Check if we can fetch the image exists (by fetching it), only then update
|
||||||
|
getData(coverUrl).then(() => {
|
||||||
|
if(CoverRef.current) CoverRef.current.src = coverUrl;
|
||||||
|
});
|
||||||
|
}, [manga, apiUri, open])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!open)
|
||||||
|
return;
|
||||||
|
LoadMaxChapter();
|
||||||
|
LoadDownloadedChapter();
|
||||||
|
LoadMangaCover();
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const [mangaMaxChapter, setMangaMaxChapter] = useState<IChapter>();
|
||||||
|
const [maxChapterLoading, setMaxChapterLoading] = useState<boolean>(true);
|
||||||
|
const LoadMaxChapter = useCallback(() => {
|
||||||
|
if(manga == null)
|
||||||
|
return;
|
||||||
|
setMaxChapterLoading(true);
|
||||||
|
GetLatestChapterAvailable(apiUri, manga.mangaId)
|
||||||
|
.then(setMangaMaxChapter)
|
||||||
|
.finally(() => setMaxChapterLoading(false));
|
||||||
|
}, [manga, apiUri]);
|
||||||
|
|
||||||
|
const [mangaDownloadedChapter, setMangaDownloadedChapter] = useState<IChapter>();
|
||||||
|
const [downloadedChapterLoading, setDownloadedChapterLoading] = useState<boolean>(true);
|
||||||
|
const LoadDownloadedChapter = useCallback(() => {
|
||||||
|
if(manga == null)
|
||||||
|
return;
|
||||||
|
setDownloadedChapterLoading(true);
|
||||||
|
GetLatestChapterDownloaded(apiUri, manga.mangaId)
|
||||||
|
.then(setMangaDownloadedChapter)
|
||||||
|
.finally(() => setDownloadedChapterLoading(false));
|
||||||
|
}, [manga, apiUri]);
|
||||||
|
|
||||||
|
const [updatingThreshold, setUpdatingThreshold] = useState<boolean>(false);
|
||||||
|
const updateIgnoreThreshold = useCallback((value: number) => {
|
||||||
|
if(manga == null)
|
||||||
|
return;
|
||||||
|
setUpdatingThreshold(true);
|
||||||
|
SetIgnoreThreshold(apiUri, manga.mangaId, value).finally(() => setUpdatingThreshold(false));
|
||||||
|
},[manga, apiUri])
|
||||||
|
|
||||||
|
const mangaConnector = useContext(MangaConnectorContext).find(all => all.name == manga?.mangaConnectorName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer anchor="bottom" size="lg" open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalClose />
|
||||||
|
<Stack direction="column" spacing={2} margin={"10px"}>
|
||||||
|
{ /* Cover and Description */ }
|
||||||
|
<Stack direction="row" spacing={2} margin={"10px"}>
|
||||||
|
<Badge sx={{margin:"8px !important"}} badgeContent={mangaConnector ? <img width={"24pt"} height={"24pt"} src={mangaConnector.iconUrl} /> : manga?.mangaConnectorName} color={ReleaseStatusToPalette(manga?.releaseStatus??MangaReleaseStatus.Unreleased)} size={"lg"}>
|
||||||
|
<img src="/blahaj.png" alt="Manga Cover"
|
||||||
|
ref={CoverRef}
|
||||||
|
onLoad={LoadMangaCover}/>
|
||||||
|
</Badge>
|
||||||
|
<Box>
|
||||||
|
<Link target={"_blank"} href={manga?.websiteUrl} level={"h2"}>
|
||||||
|
{manga?.name}
|
||||||
|
</Link>
|
||||||
|
<Stack direction={"row"} flexWrap={"wrap"} useFlexGap={true} spacing={0.3} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
|
||||||
|
{manga?.authors?.map(author => <Chip key={author.authorId} variant={"outlined"} size={"md"} color={"success"}>{author.authorName}</Chip>)}
|
||||||
|
{manga?.mangaTags?.map(tag => <Chip key={tag.tag} variant={"soft"} size={"md"} color={"primary"}>{tag.tag}</Chip>)}
|
||||||
|
{manga?.links?.map(link =>
|
||||||
|
<Chip key={link.linkId} variant={"soft"} size={"md"} color={"warning"}>
|
||||||
|
<Link target={"_blank"} sx={{textDecoration:"underline"}} level={"body-xs"} href={link?.linkUrl}>{link?.linkProvider??"Load Failed"}</Link>
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<MarkdownPreview source={manga?.description} style={{backgroundColor: "transparent", color: "var(--joy-palette-neutral-50)", maxHeight:CardHeight*0.7+"px", overflowY:"auto", marginTop:"10px", scrollbarWidth: "thin"}} />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{ /* Actions */ }
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<Input
|
||||||
|
type={"number"}
|
||||||
|
placeholder={downloadedChapterLoading ? "" : mangaDownloadedChapter?.chapterNumber??"0.0"}
|
||||||
|
startDecorator={
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
updatingThreshold ?
|
||||||
|
<CircularProgress color={"primary"} size={"sm"} />
|
||||||
|
: <Typography>Ch.</Typography>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
endDecorator={
|
||||||
|
<Typography>
|
||||||
|
<Skeleton loading={maxChapterLoading}>
|
||||||
|
/{mangaMaxChapter?.chapterNumber??"-"}
|
||||||
|
</Skeleton>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
sx={{width:"min-content"}}
|
||||||
|
size={"md"}
|
||||||
|
onChange={(e) => updateIgnoreThreshold(e.currentTarget.valueAsNumber)}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
202
tranga-website/src/Components/Search.tsx
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
CircularProgress,
|
||||||
|
Drawer,
|
||||||
|
Input,
|
||||||
|
ListItemDecorator,
|
||||||
|
Option,
|
||||||
|
Select, SelectOption,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Step,
|
||||||
|
StepIndicator,
|
||||||
|
Stepper,
|
||||||
|
Typography
|
||||||
|
} from "@mui/joy";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
import IMangaConnector from "../api/types/IMangaConnector";
|
||||||
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import {ApiUriContext} from "../api/fetchApi.tsx";
|
||||||
|
import {GetAllConnectors} from "../api/MangaConnector.tsx";
|
||||||
|
import IManga from "../api/types/IManga.ts";
|
||||||
|
import {SearchNameOnConnector, SearchUrl} from "../api/Search.tsx";
|
||||||
|
import {Manga} from "./Manga.tsx";
|
||||||
|
import Add from "@mui/icons-material/Add";
|
||||||
|
import React from "react";
|
||||||
|
import {CreateDownloadAvailableChaptersJob} from "../api/Job.tsx";
|
||||||
|
import ILocalLibrary from "../api/types/ILocalLibrary.ts";
|
||||||
|
import {GetLibraries} from "../api/LocalLibrary.tsx";
|
||||||
|
import { LibraryBooks } from "@mui/icons-material";
|
||||||
|
|
||||||
|
export default function Search({open, setOpen}:{open:boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>}){
|
||||||
|
|
||||||
|
const [step, setStep] = useState<number>(2);
|
||||||
|
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>();
|
||||||
|
const [mangaConnectorsLoading, setMangaConnectorsLoading] = useState<boolean>(true);
|
||||||
|
const [selectedMangaConnector, setSelectedMangaConnector] = useState<IMangaConnector>();
|
||||||
|
|
||||||
|
const loadMangaConnectors = useCallback(() => {
|
||||||
|
setMangaConnectorsLoading(true);
|
||||||
|
GetAllConnectors(apiUri).then(setMangaConnectors).finally(() => setMangaConnectorsLoading(false));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
const [results, setResults] = useState<IManga[]|undefined>([]);
|
||||||
|
const [resultsLoading, setResultsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const StartSearch = useCallback((mangaConnector : IMangaConnector | undefined, value: string)=>{
|
||||||
|
if(mangaConnector === undefined && !IsValidUrl(value))
|
||||||
|
return;
|
||||||
|
setResults(undefined);
|
||||||
|
setResultsLoading(true);
|
||||||
|
setStep(3);
|
||||||
|
if (IsValidUrl(value)){
|
||||||
|
SearchUrl(apiUri, value).then((r) => setResults([r])).finally(() => setResultsLoading(false));
|
||||||
|
}else if (mangaConnector != undefined){
|
||||||
|
SearchNameOnConnector(apiUri, mangaConnector.name, value).then(setResults).finally(() => setResultsLoading(false));
|
||||||
|
}
|
||||||
|
},[apiUri])
|
||||||
|
|
||||||
|
function IsValidUrl(str : string) : boolean {
|
||||||
|
const pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
|
||||||
|
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
|
||||||
|
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
|
||||||
|
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
|
||||||
|
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
|
||||||
|
'(\\#[-a-z\\d_]*)?$','i'); // fragment locator
|
||||||
|
return !!pattern.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [localLibraries, setLocalLibraries] = useState<ILocalLibrary[]>();
|
||||||
|
const [localLibrariesLoading, setLocalLibrariesLoading] = useState<boolean>(true);
|
||||||
|
const [selectedLibraryId, setSelectedLibraryId] = useState<string>();
|
||||||
|
|
||||||
|
const loadLocalLibraries = useCallback(() => {
|
||||||
|
setLocalLibrariesLoading(true);
|
||||||
|
GetLibraries(apiUri).then(setLocalLibraries).finally(() => setLocalLibrariesLoading(false));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadMangaConnectors();
|
||||||
|
loadLocalLibraries();
|
||||||
|
},[apiUri]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadMangaConnectors();
|
||||||
|
loadLocalLibraries();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function renderValue(option: SelectOption<string> | null) {
|
||||||
|
if (!option) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<ListItemDecorator>
|
||||||
|
<Avatar size="sm" src={mangaConnectors?.find((o) => o.name === option.value)?.iconUrl} />
|
||||||
|
</ListItemDecorator>
|
||||||
|
{option.label}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return (
|
||||||
|
<Drawer size={"lg"} anchor={"right"} open={open} onClose={() => {
|
||||||
|
if(step > 2)
|
||||||
|
setStep(2);
|
||||||
|
setResults([]);
|
||||||
|
setOpen(false);
|
||||||
|
}}>
|
||||||
|
<ModalClose />
|
||||||
|
<Stepper orientation={"vertical"} sx={{ height: '100%', width: "calc(100% - 80px)", margin:"40px"}}>
|
||||||
|
<Step indicator={
|
||||||
|
<StepIndicator variant={step==1?"solid":"outlined"} color={(mangaConnectors?.length??0) < 1 ? "danger" : "primary"}>
|
||||||
|
1
|
||||||
|
</StepIndicator>}>
|
||||||
|
<Skeleton loading={mangaConnectorsLoading}>
|
||||||
|
<Select
|
||||||
|
color={(mangaConnectors?.length??0) < 1 ? "danger" : "neutral"}
|
||||||
|
disabled={mangaConnectorsLoading || resultsLoading || mangaConnectors?.length == null || mangaConnectors.length < 1}
|
||||||
|
placeholder={"Select Connector"}
|
||||||
|
slotProps={{
|
||||||
|
listbox: {
|
||||||
|
sx: {
|
||||||
|
'--ListItemDecorator-size': '44px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{ '--ListItemDecorator-size': '44px', minWidth: 240 }}
|
||||||
|
renderValue={renderValue}
|
||||||
|
onChange={(_e, newValue) => {
|
||||||
|
setSelectedMangaConnector(mangaConnectors?.find((o) => o.name === newValue));
|
||||||
|
setStep(2);
|
||||||
|
setResults(undefined);
|
||||||
|
}}
|
||||||
|
endDecorator={<Chip size={"sm"} color={mangaConnectors?.length??0 < 1 ? "danger" : "primary"}>{mangaConnectors?.length}</Chip>}>
|
||||||
|
{mangaConnectors?.map((connector: IMangaConnector) => ConnectorOption(connector))}
|
||||||
|
</Select>
|
||||||
|
</Skeleton>
|
||||||
|
</Step>
|
||||||
|
<Step indicator={
|
||||||
|
<StepIndicator variant={step==2?"solid":"outlined"} color="primary">
|
||||||
|
2
|
||||||
|
</StepIndicator>}>
|
||||||
|
<Input disabled={resultsLoading} placeholder={"Name or Url " + (selectedMangaConnector ? selectedMangaConnector.baseUris[0] : "")} onKeyDown={(e) => {
|
||||||
|
setStep(2);
|
||||||
|
setResults(undefined);
|
||||||
|
if(e.key === "Enter") {
|
||||||
|
StartSearch(selectedMangaConnector, e.currentTarget.value);
|
||||||
|
}
|
||||||
|
}}/>
|
||||||
|
</Step>
|
||||||
|
<Step indicator={
|
||||||
|
<StepIndicator variant={step==3?"solid":"outlined"} color="primary">
|
||||||
|
3
|
||||||
|
</StepIndicator>}>
|
||||||
|
<Typography endDecorator={<Chip size={"sm"} color={"primary"}>{results?.length??"-"}</Chip>}>Results</Typography>
|
||||||
|
<Skeleton loading={resultsLoading}>
|
||||||
|
<Stack direction={"row"} spacing={1} flexWrap={"wrap"}>
|
||||||
|
{results?.map((result) =>
|
||||||
|
<Manga key={result.mangaId} manga={result}>
|
||||||
|
<Select
|
||||||
|
placeholder={"Select Library"}
|
||||||
|
defaultValue={""}
|
||||||
|
startDecorator={<LibraryBooks />}
|
||||||
|
value={selectedLibraryId}
|
||||||
|
onChange={(_e, newValue) => setSelectedLibraryId(newValue!)}>
|
||||||
|
{localLibrariesLoading ?
|
||||||
|
<Option value={""} disabled>Loading <CircularProgress color={"primary"} size={"sm"} /></Option>
|
||||||
|
:
|
||||||
|
(localLibraries??[]).map(library => {
|
||||||
|
return (
|
||||||
|
<Option value={library.localLibraryId}>{library.libraryName} ({library.basePath})</Option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
<Button disabled={localLibrariesLoading || selectedLibraryId === undefined} onClick={() => {
|
||||||
|
CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {localLibraryId: selectedLibraryId!,recurrenceTimeMs: 1000 * 60 * 60 * 3, language: "en"})
|
||||||
|
}} endDecorator={<Add />}>Watch</Button>
|
||||||
|
</Manga>)}
|
||||||
|
</Stack>
|
||||||
|
</Skeleton>
|
||||||
|
</Step>
|
||||||
|
</Stepper>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConnectorOption(connector: IMangaConnector){
|
||||||
|
return (
|
||||||
|
<Option key={connector.name} value={connector.name} sx={{position: "relative"}}>
|
||||||
|
<ListItemDecorator>
|
||||||
|
<Avatar size="sm" src={connector.iconUrl} />
|
||||||
|
</ListItemDecorator>
|
||||||
|
{connector.name}
|
||||||
|
</Option>
|
||||||
|
);
|
||||||
|
}
|
59
tranga-website/src/Components/Settings/AprilFoolsMode.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import IBackendSettings from "../../api/types/IBackendSettings.ts";
|
||||||
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
ColorPaletteProp,
|
||||||
|
Switch,
|
||||||
|
Typography
|
||||||
|
} from "@mui/joy";
|
||||||
|
import * as React from "react";
|
||||||
|
import {GetAprilFoolsToggle, UpdateAprilFoolsToggle} from "../../api/BackendSettings.tsx";
|
||||||
|
|
||||||
|
export default function ImageProcessing({backendSettings}: {backendSettings?: IBackendSettings}) {
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
const [value, setValue] = useState<boolean>(backendSettings?.aprilFoolsMode??false);
|
||||||
|
|
||||||
|
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const valueChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setColor("warning");
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
UpdateAprilFoolsMode(e.target.checked);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(backendSettings?.aprilFoolsMode??false);
|
||||||
|
}, [backendSettings]);
|
||||||
|
|
||||||
|
const UpdateAprilFoolsMode = useCallback((val: boolean) => {
|
||||||
|
UpdateAprilFoolsToggle(apiUri, val)
|
||||||
|
.then(() => GetAprilFoolsToggle(apiUri))
|
||||||
|
.then((val) => setValue(val))
|
||||||
|
.then(() => setColor("success"))
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary>April Fools Mode</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Typography endDecorator={
|
||||||
|
<Switch disabled={backendSettings === undefined || loading}
|
||||||
|
onChange={valueChanged}
|
||||||
|
color={color}
|
||||||
|
checked={value} />
|
||||||
|
}>
|
||||||
|
Toggle
|
||||||
|
</Typography>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
import IBackendSettings from "../../api/types/IBackendSettings";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary, Chip,
|
||||||
|
CircularProgress,
|
||||||
|
ColorPaletteProp,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
Stack, Tooltip, Typography
|
||||||
|
} from "@mui/joy";
|
||||||
|
import {KeyboardEventHandler, useCallback, useContext, useState} from "react";
|
||||||
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
|
import {UpdateChapterNamingScheme} from "../../api/BackendSettings.tsx";
|
||||||
|
|
||||||
|
export default function ChapterNamingScheme({backendSettings}: {backendSettings?: IBackendSettings}) {
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [value, setValue] = useState<string>("");
|
||||||
|
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
|
||||||
|
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
|
||||||
|
if(e.key === "Enter") {
|
||||||
|
setLoading(true);
|
||||||
|
UpdateChapterNamingScheme(apiUri, value)
|
||||||
|
.then(() => setColor("success"))
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}
|
||||||
|
}, [apiUri])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary>Chapter Naming Scheme</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Input disabled={backendSettings === undefined || loading}
|
||||||
|
placeholder={"Chapter Naming Scheme"}
|
||||||
|
defaultValue={backendSettings?.chapterNamingScheme}
|
||||||
|
onKeyDown={keyDown}
|
||||||
|
onChange={e => setValue(e.target.value)}
|
||||||
|
color={color}
|
||||||
|
endDecorator={(loading ? <CircularProgress color={"primary"} size={"sm"} /> : null)}
|
||||||
|
/>
|
||||||
|
<Typography level={"title-sm"}>Placeholders:</Typography>
|
||||||
|
<Stack direction="row" spacing={1} divider={<Divider />}>
|
||||||
|
<Tooltip arrow placement="bottom" size="md" variant="outlined"
|
||||||
|
title={"Manga Title"} >
|
||||||
|
<Chip color={"primary"}>%M</Chip>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip arrow placement="bottom" size="md" variant="outlined"
|
||||||
|
title={"Volume Number"} >
|
||||||
|
<Chip color={"primary"}>%V</Chip>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip arrow placement="bottom" size="md" variant="outlined"
|
||||||
|
title={"Chapter Number"} >
|
||||||
|
<Chip color={"primary"}>%C</Chip>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip arrow placement="bottom" size="md" variant="outlined"
|
||||||
|
title={"Chapter Title"} >
|
||||||
|
<Chip color={"primary"}>%T</Chip>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip arrow placement="bottom" size="md" variant="outlined"
|
||||||
|
title={"Year"} >
|
||||||
|
<Chip color={"primary"}>%Y</Chip>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip arrow placement="bottom" size="md" variant="outlined"
|
||||||
|
title={"First Author"} >
|
||||||
|
<Chip color={"primary"}>%A</Chip>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
72
tranga-website/src/Components/Settings/FlareSolverr.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import IBackendSettings from "../../api/types/IBackendSettings";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Button,
|
||||||
|
ColorPaletteProp,
|
||||||
|
Input, Stack
|
||||||
|
} from "@mui/joy";
|
||||||
|
import {KeyboardEventHandler, useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
|
import {
|
||||||
|
ResetFlareSolverrUrl,
|
||||||
|
SetFlareSolverrUrl, TestFlareSolverrUrl,
|
||||||
|
} from "../../api/BackendSettings.tsx";
|
||||||
|
|
||||||
|
export default function FlareSolverr({backendSettings}: {backendSettings?: IBackendSettings}) {
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [value, setValue] = useState<string>(backendSettings?.flareSolverrUrl??"");
|
||||||
|
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
|
||||||
|
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
|
||||||
|
if(value === undefined) return;
|
||||||
|
if(e.key === "Enter") {
|
||||||
|
setLoading(true);
|
||||||
|
SetFlareSolverrUrl(apiUri, value)
|
||||||
|
.then(() => setColor("success"))
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}
|
||||||
|
}, [apiUri, value])
|
||||||
|
|
||||||
|
const Reset = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
|
ResetFlareSolverrUrl(apiUri)
|
||||||
|
.then(() => Test())
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
const Test = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
|
TestFlareSolverrUrl(apiUri)
|
||||||
|
.then(() => setColor("success"))
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(backendSettings?.flareSolverrUrl??"");
|
||||||
|
}, [backendSettings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary>FlareSolverr</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Input disabled={backendSettings === undefined || loading}
|
||||||
|
placeholder={"FlareSolverr URL"}
|
||||||
|
value={value}
|
||||||
|
onKeyDown={keyDown}
|
||||||
|
onChange={e => setValue(e.target.value)}
|
||||||
|
color={color}
|
||||||
|
endDecorator={<Stack direction={"row"} spacing={1}>
|
||||||
|
<Button onClick={Reset} loading={loading}>Reset</Button>
|
||||||
|
<Button onClick={Test} loading={loading}>Test</Button>
|
||||||
|
</Stack>}
|
||||||
|
/>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
102
tranga-website/src/Components/Settings/ImageProcessing.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import IBackendSettings from "../../api/types/IBackendSettings.ts";
|
||||||
|
import {ChangeEvent, useCallback, useContext, useEffect, useRef, useState} from "react";
|
||||||
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary, ColorPaletteProp, Input, Stack, Switch, Typography,
|
||||||
|
} from "@mui/joy";
|
||||||
|
import {
|
||||||
|
GetBWImageToggle,
|
||||||
|
GetImageCompressionValue,
|
||||||
|
UpdateBWImageToggle,
|
||||||
|
UpdateImageCompressionValue
|
||||||
|
} from "../../api/BackendSettings.tsx";
|
||||||
|
|
||||||
|
export default function ImageProcessing ({backendSettings}: { backendSettings?: IBackendSettings }) {
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBwImages(backendSettings?.bwImages??false);
|
||||||
|
setCompression(backendSettings?.compression??100);
|
||||||
|
}, [backendSettings]);
|
||||||
|
|
||||||
|
const [bwImages, setBwImages] = useState<boolean>(backendSettings?.bwImages??false);
|
||||||
|
const [bwImagesLoading, setBwImagesLoading] = useState(false);
|
||||||
|
const [bwImagesColor, setBwImagesColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
const bwTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const bwValueChanged = (e : ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setBwImages(e.target.checked);
|
||||||
|
setBwImagesColor("warning");
|
||||||
|
clearTimeout(bwTimerRef.current);
|
||||||
|
bwTimerRef.current = setTimeout(() => {
|
||||||
|
UpdateBwImages(e.target.checked);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
const UpdateBwImages = useCallback((val : boolean) => {
|
||||||
|
setBwImagesLoading(true);
|
||||||
|
UpdateBWImageToggle(apiUri, val)
|
||||||
|
.then(() => GetBWImageToggle(apiUri))
|
||||||
|
.then(setBwImages)
|
||||||
|
.then(() => setBwImagesColor("success"))
|
||||||
|
.catch(() => setBwImagesColor("danger"))
|
||||||
|
.finally(() => setBwImagesLoading(false));
|
||||||
|
},[apiUri]);
|
||||||
|
|
||||||
|
const [compression, setCompression] = useState<number>(backendSettings?.compression??100);
|
||||||
|
const [compressionLoading, setCompressionLoading] = useState(false);
|
||||||
|
const [compressionColor, setCompressionColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
const compressionTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const compressionCheckedChanged = (e : ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setCompressionColor("warning");
|
||||||
|
if(!e.target.checked)
|
||||||
|
setCompression(100);
|
||||||
|
else
|
||||||
|
setCompression(50);
|
||||||
|
clearTimeout(compressionTimerRef.current);
|
||||||
|
bwTimerRef.current = setTimeout(() => {
|
||||||
|
UpdateImageCompression(e.target.checked ? 50 : 100);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
const compressionValueChanged = (e : ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setCompressionColor("warning");
|
||||||
|
setCompression(parseInt(e.target.value));
|
||||||
|
clearTimeout(compressionTimerRef.current);
|
||||||
|
bwTimerRef.current = setTimeout(() => {
|
||||||
|
UpdateImageCompression(parseInt(e.target.value));
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
const UpdateImageCompression = useCallback((val : number) => {
|
||||||
|
setCompressionLoading(true);
|
||||||
|
UpdateImageCompressionValue(apiUri, val)
|
||||||
|
.then(() => GetImageCompressionValue(apiUri))
|
||||||
|
.then(setCompression)
|
||||||
|
.then(() => setCompressionColor("success"))
|
||||||
|
.catch(() => setCompressionColor("danger"))
|
||||||
|
.finally(() => setCompressionLoading(false));
|
||||||
|
},[apiUri]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary>Image Processing</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Stack>
|
||||||
|
<Typography endDecorator={
|
||||||
|
<Switch disabled={backendSettings === undefined || bwImagesLoading}
|
||||||
|
onChange={bwValueChanged}
|
||||||
|
color={bwImagesColor}
|
||||||
|
checked={bwImages} />
|
||||||
|
}>B/W Images</Typography>
|
||||||
|
<Typography endDecorator={
|
||||||
|
<Input type={"number"} value={compression} onChange={compressionValueChanged} startDecorator={
|
||||||
|
<Switch disabled={backendSettings === undefined || compressionLoading}
|
||||||
|
onChange={compressionCheckedChanged}
|
||||||
|
color={compressionColor}
|
||||||
|
checked={compression < 100} />
|
||||||
|
} />
|
||||||
|
}>Compression</Typography>
|
||||||
|
</Stack>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
81
tranga-website/src/Components/Settings/RequestLimits.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import IBackendSettings from "../../api/types/IBackendSettings.ts";
|
||||||
|
import {useCallback, useContext, useState} from "react";
|
||||||
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Button,
|
||||||
|
ColorPaletteProp,
|
||||||
|
Input,
|
||||||
|
Stack,
|
||||||
|
Typography
|
||||||
|
} from "@mui/joy";
|
||||||
|
import {RequestLimitType} from "../../api/types/EnumRequestLimitType.ts";
|
||||||
|
import {ResetRequestLimit, ResetRequestLimits, UpdateRequestLimit} from "../../api/BackendSettings.tsx";
|
||||||
|
import {Restore} from "@mui/icons-material";
|
||||||
|
|
||||||
|
export default function RequestLimits({backendSettings}: {backendSettings?: IBackendSettings}) {
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
|
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const Update = useCallback((target: HTMLInputElement, limit: RequestLimitType) => {
|
||||||
|
setLoading(true);
|
||||||
|
UpdateRequestLimit(apiUri, limit, Number.parseInt(target.value))
|
||||||
|
.then(() => setColor("success"))
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
},[apiUri])
|
||||||
|
|
||||||
|
const Reset = useCallback((limit: RequestLimitType) => {
|
||||||
|
setLoading(true);
|
||||||
|
ResetRequestLimit(apiUri, limit)
|
||||||
|
.then(() => setColor("success"))
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
const ResetAll = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
|
ResetRequestLimits(apiUri)
|
||||||
|
.then(() => setColor("success"))
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary>Request Limits</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Stack spacing={1} direction="column">
|
||||||
|
<Button loading={backendSettings === undefined} onClick={ResetAll} size={"sm"} variant={"outlined"} endDecorator={<Restore />} color={"warning"}>Reset all</Button>
|
||||||
|
<Item type={RequestLimitType.Default} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
|
||||||
|
<Item type={RequestLimitType.MangaInfo} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
|
||||||
|
<Item type={RequestLimitType.MangaImage} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
|
||||||
|
<Item type={RequestLimitType.MangaDexFeed} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
|
||||||
|
<Item type={RequestLimitType.MangaDexImage} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
|
||||||
|
</Stack>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Item({type, color, loading, backendSettings, Reset, Update}:
|
||||||
|
{type: RequestLimitType, color: ColorPaletteProp, loading: boolean, backendSettings: IBackendSettings | undefined, Reset: (x: RequestLimitType) => void, Update: (a: HTMLInputElement, x: RequestLimitType) => void}) {
|
||||||
|
return (
|
||||||
|
<Input slotProps={{input: {min: 0, max: 360}}}
|
||||||
|
color={color}
|
||||||
|
startDecorator={<Typography sx={{width:"140px"}}>{type}</Typography>}
|
||||||
|
endDecorator={<Button onClick={() => Reset(type)}>Reset</Button>}
|
||||||
|
disabled={loading} type={"number"}
|
||||||
|
defaultValue={backendSettings?.requestLimits[type]}
|
||||||
|
placeholder={"Default"}
|
||||||
|
required
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if(e.key == "Enter")
|
||||||
|
Update(e.target as HTMLInputElement, type);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
60
tranga-website/src/Components/Settings/UserAgent.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import IBackendSettings from "../../api/types/IBackendSettings";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Button,
|
||||||
|
ColorPaletteProp,
|
||||||
|
Input
|
||||||
|
} from "@mui/joy";
|
||||||
|
import {KeyboardEventHandler, useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
|
import {GetUserAgent, ResetUserAgent, UpdateUserAgent} from "../../api/BackendSettings.tsx";
|
||||||
|
|
||||||
|
export default function UserAgent({backendSettings}: {backendSettings?: IBackendSettings}) {
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [value, setValue] = useState<string>(backendSettings?.userAgent??"");
|
||||||
|
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
|
||||||
|
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
|
||||||
|
if(value === undefined) return;
|
||||||
|
if(e.key === "Enter") {
|
||||||
|
setLoading(true);
|
||||||
|
UpdateUserAgent(apiUri, value)
|
||||||
|
.then(() => setColor("success"))
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}
|
||||||
|
}, [apiUri, value])
|
||||||
|
|
||||||
|
const Reset = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
|
ResetUserAgent(apiUri)
|
||||||
|
.then(() => GetUserAgent(apiUri))
|
||||||
|
.then((val) => setValue(val))
|
||||||
|
.then(() => setColor("success"))
|
||||||
|
.catch(() => setColor("danger"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(backendSettings?.userAgent??"");
|
||||||
|
}, [backendSettings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary>UserAgent</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Input disabled={backendSettings === undefined || loading}
|
||||||
|
placeholder={"UserAgent"}
|
||||||
|
value={value}
|
||||||
|
onKeyDown={keyDown}
|
||||||
|
onChange={e => setValue(e.target.value)}
|
||||||
|
color={color}
|
||||||
|
endDecorator={<Button onClick={Reset} loading={loading}>Reset</Button>}
|
||||||
|
/>
|
||||||
|
</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;
|
||||||
|
}
|
28
tranga-website/src/Header.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import Sheet from "@mui/joy/Sheet";
|
||||||
|
import {Box, Link, Stack, Typography} from "@mui/joy";
|
||||||
|
import {ReactElement} from "react";
|
||||||
|
import './Header.css';
|
||||||
|
import { GitHub } from "@mui/icons-material";
|
||||||
|
|
||||||
|
export default function Header({children} : {children? : ReactElement<any, any> | ReactElement<any,any>[] | undefined}) : ReactElement {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet className={"header"}>
|
||||||
|
<Stack direction={"row"} spacing={2} sx={{width: "100%", alignItems: "center"}}>
|
||||||
|
{children}
|
||||||
|
<Box sx={{flexGrow: 1}} />
|
||||||
|
<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>
|
||||||
|
<Box sx={{flexGrow: 1}} />
|
||||||
|
<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>
|
||||||
|
</Stack>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
0
tranga-website/src/Settings.css
Normal file
127
tranga-website/src/Settings.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import Drawer from '@mui/joy/Drawer';
|
||||||
|
import ModalClose from '@mui/joy/ModalClose';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionGroup,
|
||||||
|
AccordionSummary, CircularProgress, ColorPaletteProp,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle, Input,
|
||||||
|
Link, Stack
|
||||||
|
} from "@mui/joy";
|
||||||
|
import './Settings.css';
|
||||||
|
import * as React from "react";
|
||||||
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import {ApiUriContext} from "./api/fetchApi.tsx";
|
||||||
|
import IBackendSettings from "./api/types/IBackendSettings.ts";
|
||||||
|
import { GetSettings } from './api/BackendSettings.tsx';
|
||||||
|
import UserAgent from "./Components/Settings/UserAgent.tsx";
|
||||||
|
import ImageProcessing from "./Components/Settings/ImageProcessing.tsx";
|
||||||
|
import ChapterNamingScheme from "./Components/Settings/ChapterNamingScheme.tsx";
|
||||||
|
import AprilFoolsMode from './Components/Settings/AprilFoolsMode.tsx';
|
||||||
|
import RequestLimits from "./Components/Settings/RequestLimits.tsx";
|
||||||
|
import FlareSolverr from "./Components/Settings/FlareSolverr.tsx";
|
||||||
|
import {Article} from '@mui/icons-material';
|
||||||
|
|
||||||
|
const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
||||||
|
return fetch(`${apiUri}/swagger/v2/swagger.json`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if(!(response.ok && response.status == 200))
|
||||||
|
return false;
|
||||||
|
return response.json().then((json) => (json as {openapi:string}).openapi.match("[0-9]+(?:\.[0-9]+)+")?true:false).catch(() => false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Settings({open, setOpen, setApiUri, setConnected}:{open:boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>, setApiUri:React.Dispatch<React.SetStateAction<string>>, setConnected:React.Dispatch<React.SetStateAction<boolean>>}) {
|
||||||
|
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
|
const [apiUriColor, setApiUriColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
|
||||||
|
const [apiUriAccordionOpen, setApiUriAccordionOpen] = React.useState(true);
|
||||||
|
const [checking, setChecking] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
OnCheckConnection(apiUri);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const apiUriChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
setApiUriColor("warning");
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
OnCheckConnection(e.target.value);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const OnCheckConnection = (uri: string) => {
|
||||||
|
console.log("Checking connection...");
|
||||||
|
setChecking(true);
|
||||||
|
checkConnection(uri)
|
||||||
|
.then((result) => {
|
||||||
|
setConnected(result);
|
||||||
|
if(result)
|
||||||
|
console.log("Connected!");
|
||||||
|
setApiUriAccordionOpen(!result);
|
||||||
|
setApiUriColor(result ? "success" : "danger");
|
||||||
|
if(result)
|
||||||
|
setApiUri(uri);
|
||||||
|
})
|
||||||
|
.finally(() => setChecking(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
const [backendSettings, setBackendSettings] = useState<IBackendSettings>();
|
||||||
|
|
||||||
|
const getBackendSettings = useCallback(() => {
|
||||||
|
GetSettings(apiUri).then(setBackendSettings);
|
||||||
|
}, [apiUri]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getBackendSettings();
|
||||||
|
}, [checking]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer size={"lg"} open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalClose />
|
||||||
|
<DialogTitle>Settings</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion expanded={apiUriAccordionOpen} onChange={(_e, expanded) => setApiUriAccordionOpen(expanded)}>
|
||||||
|
<AccordionSummary>ApiUri</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Input
|
||||||
|
disabled={checking}
|
||||||
|
color={apiUriColor}
|
||||||
|
placeholder={"http(s)://"}
|
||||||
|
type={"url"}
|
||||||
|
defaultValue={apiUri}
|
||||||
|
onChange={apiUriChanged}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if(e.key === "Enter") {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
OnCheckConnection(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
endDecorator={(checking ? <CircularProgress color={apiUriColor} size={"sm"} /> : null)} />
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
<UserAgent backendSettings={backendSettings} />
|
||||||
|
<ImageProcessing backendSettings={backendSettings} />
|
||||||
|
<ChapterNamingScheme backendSettings={backendSettings} />
|
||||||
|
<AprilFoolsMode backendSettings={backendSettings} />
|
||||||
|
<RequestLimits backendSettings={backendSettings} />
|
||||||
|
<FlareSolverr backendSettings={backendSettings} />
|
||||||
|
</AccordionGroup>
|
||||||
|
<Stack spacing={2} direction="row">
|
||||||
|
<Link target={"_blank"} href={apiUri + "/swagger"}><Article />Swagger Doc</Link>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
92
tranga-website/src/api/BackendSettings.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import {deleteData, getData, patchData, postData} from './fetchApi.tsx';
|
||||||
|
import IBackendSettings from "./types/IBackendSettings.ts";
|
||||||
|
import IRequestLimits from "./types/IRequestLimits.ts";
|
||||||
|
import {RequestLimitType} from "./types/EnumRequestLimitType.ts";
|
||||||
|
|
||||||
|
export const GetSettings = async (apiUri: string) : Promise<IBackendSettings> => {
|
||||||
|
return await getData(`${apiUri}/v2/Settings`) as Promise<IBackendSettings>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetUserAgent = async (apiUri: string) : Promise<string> => {
|
||||||
|
return await getData(`${apiUri}/v2/Settings/UserAgent`) as Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateUserAgent = async (apiUri: string, userAgent: string)=> {
|
||||||
|
if(userAgent === undefined || userAgent === null)
|
||||||
|
return Promise.reject(`userAgent was not provided`);
|
||||||
|
return patchData(`${apiUri}/v2/Settings/UserAgent`, userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResetUserAgent = async (apiUri: string) => {
|
||||||
|
return deleteData(`${apiUri}/v2/Settings/UserAgent`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetRequestLimits = async(apiUri: string) : Promise<IRequestLimits> => {
|
||||||
|
return await getData(`${apiUri}/v2/Settings/RequestLimits`) as Promise<IRequestLimits>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResetRequestLimits = async (apiUri: string) => {
|
||||||
|
return deleteData(`${apiUri}/v2/Settings/RequestLimits`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateRequestLimit = async (apiUri: string, requestType: RequestLimitType, value: number) => {
|
||||||
|
if(requestType === undefined || requestType === null || value === undefined || value === null)
|
||||||
|
return Promise.reject();
|
||||||
|
return patchData(`${apiUri}/v2/Settings/RequestLimits/${requestType}`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResetRequestLimit = async (apiUri: string, requestType: RequestLimitType) => {
|
||||||
|
if(requestType === undefined || requestType === null)
|
||||||
|
return Promise.reject("requestType was not provided");
|
||||||
|
return deleteData(`${apiUri}/v2/Settings/RequestLimits/${requestType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetImageCompressionValue = async (apiUri: string) : Promise<number> => {
|
||||||
|
return await getData(`${apiUri}/v2/Settings/ImageCompression`) as Promise<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateImageCompressionValue = async (apiUri: string, value: number) => {
|
||||||
|
if(value === undefined || value === null)
|
||||||
|
return Promise.reject("value was not provided");
|
||||||
|
return patchData(`${apiUri}/v2/Settings/ImageCompression`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetBWImageToggle = async (apiUri: string) : Promise<boolean> => {
|
||||||
|
return await getData(`${apiUri}/v2/Settings/BWImages`) as Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateBWImageToggle = async (apiUri: string, value: boolean) => {
|
||||||
|
if(value === undefined || value === null)
|
||||||
|
return Promise.reject("value was not provided");
|
||||||
|
return patchData(`${apiUri}/v2/Settings/BWImages`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetAprilFoolsToggle = async (apiUri: string) : Promise<boolean> => {
|
||||||
|
return await getData(`${apiUri}/v2/Settings/AprilFoolsMode`) as Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateAprilFoolsToggle = async (apiUri: string, value: boolean) => {
|
||||||
|
if(value === undefined || value === null)
|
||||||
|
return Promise.reject("value was not provided");
|
||||||
|
return patchData(`${apiUri}/v2/Settings/AprilFoolsMode`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetChapterNamingScheme = async (apiUri: string) : Promise<string> => {
|
||||||
|
return await getData(`${apiUri}/v2/Settings/ChapterNamingScheme`) as Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateChapterNamingScheme = async (apiUri: string, value: string) => {
|
||||||
|
return patchData(`${apiUri}/v2/Settings/ChapterNamingScheme`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SetFlareSolverrUrl = async (apiUri: string, value: string) => {
|
||||||
|
return postData(`${apiUri}/v2/Settings/FlareSolverr/Url`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResetFlareSolverrUrl = async (apiUri: string) => {
|
||||||
|
return deleteData(`${apiUri}/v2/Settings/FlareSolverr/Url`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TestFlareSolverrUrl = async (apiUri: string) => {
|
||||||
|
return postData(`${apiUri}/v2/Settings/FlareSolverr/Test`);
|
||||||
|
}
|
8
tranga-website/src/api/Chapter.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import {getData} from "./fetchApi.tsx";
|
||||||
|
import IChapter from "./types/IChapter.ts";
|
||||||
|
|
||||||
|
export const GetChapterFromId = async (apiUri: string, chapterId: string): Promise<IChapter> => {
|
||||||
|
if(chapterId === undefined || chapterId === null)
|
||||||
|
return Promise.reject(`chapterId was not provided`);
|
||||||
|
return await getData(`${apiUri}/v2/Query/Chapter/${chapterId}`) as Promise<IChapter>;
|
||||||
|
}
|
9
tranga-website/src/api/Contexts/ChapterContext.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import {createContext} from "react";
|
||||||
|
import IChapter from "../types/IChapter.ts";
|
||||||
|
|
||||||
|
export const ChapterContext = createContext<{chapters: IChapter[], GetChapter: (chapterId: string) => Promise<IChapter | undefined>}>(
|
||||||
|
{
|
||||||
|
chapters : [],
|
||||||
|
GetChapter: _ => Promise.resolve(undefined)
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,4 @@
|
|||||||
|
import {createContext} from "react";
|
||||||
|
import IMangaConnector from "../types/IMangaConnector.ts";
|
||||||
|
|
||||||
|
export const MangaConnectorContext = createContext<IMangaConnector[]>([]);
|
9
tranga-website/src/api/Contexts/MangaContext.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import {createContext} from "react";
|
||||||
|
import IManga, {DefaultManga} from "../types/IManga.ts";
|
||||||
|
|
||||||
|
export const MangaContext = createContext<{mangas: IManga[], GetManga: (mangaId: string) => Promise<IManga | undefined>}>(
|
||||||
|
{
|
||||||
|
mangas : [],
|
||||||
|
GetManga: _ => Promise.resolve(DefaultManga)
|
||||||
|
}
|
||||||
|
);
|
97
tranga-website/src/api/Job.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import {deleteData, getData, patchData, postData, putData} from "./fetchApi";
|
||||||
|
import IJob, {JobState, JobType} from "./types/Jobs/IJob";
|
||||||
|
import IModifyJobRecord from "./types/records/IModifyJobRecord";
|
||||||
|
import IDownloadAvailableChaptersJobRecord from "./types/records/IDownloadAvailableChaptersJobRecord.ts";
|
||||||
|
|
||||||
|
export const GetAllJobs = async (apiUri: string) : Promise<IJob[]> => {
|
||||||
|
return await getData(`${apiUri}/v2/Job`) as Promise<IJob[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetJobsWithIds = async (apiUri: string, jobIds: string[]) : Promise<IJob[]> => {
|
||||||
|
if(jobIds === null || jobIds === undefined || jobIds.length === 0)
|
||||||
|
return Promise.reject("jobIds was not provided");
|
||||||
|
return await postData(`${apiUri}/v2/Job/WithIDs`, jobIds) as Promise<IJob[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetJobsInState = async (apiUri: string, state: JobState) : Promise<IJob[]> => {
|
||||||
|
if(state == null)
|
||||||
|
return Promise.reject("state was not provided");
|
||||||
|
return await getData(`${apiUri}/v2/Job/State/${state}`) as Promise<IJob[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetJobsWithType = async (apiUri: string, jobType: JobType) : Promise<IJob[]> => {
|
||||||
|
if(jobType == null) {
|
||||||
|
return Promise.reject("jobType was not provided");
|
||||||
|
}
|
||||||
|
return await getData(`${apiUri}/v2/Job/Type/${jobType}`) as Promise<IJob[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetJobsOfTypeAndWithState = async (apiUri: string, jobType: JobType, state: JobState) : Promise<IJob[]> => {
|
||||||
|
if(jobType == null)
|
||||||
|
return Promise.reject("jobType was not provided");
|
||||||
|
if(state == null)
|
||||||
|
return Promise.reject("state was not provided");
|
||||||
|
return await getData(`${apiUri}/v2/Job/TypeAndState/${jobType}/${state}`) as Promise<IJob[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetJob = async (apiUri: string, jobId: string) : Promise<IJob> => {
|
||||||
|
if(jobId === undefined || jobId === null || jobId.length < 1)
|
||||||
|
return Promise.reject("jobId was not provided");
|
||||||
|
return await getData(`${apiUri}/v2/Job/${jobId}`) as Promise<IJob>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteJob = async (apiUri: string, jobId: string) : Promise<void> => {
|
||||||
|
if(jobId === undefined || jobId === null || jobId.length < 1)
|
||||||
|
return Promise.reject("jobId was not provided");
|
||||||
|
return await deleteData(`${apiUri}/v2/Job/${jobId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModifyJob = async (apiUri: string, jobId: string, modifyData: IModifyJobRecord) : Promise<IJob> => {
|
||||||
|
if(jobId === undefined || jobId === null || jobId.length < 1)
|
||||||
|
return Promise.reject("jobId was not provided");
|
||||||
|
if(modifyData === undefined || modifyData === null)
|
||||||
|
return Promise.reject("modifyData was not provided");
|
||||||
|
return await patchData(`${apiUri}/v2/Job/${jobId}`, modifyData) as Promise<IJob>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateDownloadAvailableChaptersJob = async (apiUri: string, mangaId: string, data: IDownloadAvailableChaptersJobRecord) : Promise<string[]> => {
|
||||||
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
if(data === undefined || data === null)
|
||||||
|
return Promise.reject("data was not provided");
|
||||||
|
return await putData(`${apiUri}/v2/Job/DownloadAvailableChaptersJob/${mangaId}`, data) as Promise<string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateDownloadSingleChapterJob = async (apiUri: string, chapterId: string) : Promise<string[]> => {
|
||||||
|
if(chapterId === undefined || chapterId === null || chapterId.length < 1)
|
||||||
|
return Promise.reject("chapterId was not provided");
|
||||||
|
return await putData(`${apiUri}/v2/Job/DownloadSingleChapterJob/${chapterId}`, {}) as Promise<string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateUpdateFilesJob = async (apiUri: string, mangaId: string) : Promise<string[]> => {
|
||||||
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
return await putData(`${apiUri}/v2/Job/UpdateFilesJob/${mangaId}`, {}) as Promise<string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateUpdateAllFilesJob = async (apiUri: string) : Promise<string[]> => {
|
||||||
|
return await putData(`${apiUri}/v2/Job/UpdateAllFilesJob`, {}) as Promise<string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateUpdateMetadataJob = async (apiUri: string, mangaId: string) : Promise<string[]> => {
|
||||||
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
return await putData(`${apiUri}/v2/Job/UpdateMetadataJob/${mangaId}`, {}) as Promise<string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateUpdateAllMetadataJob = async (apiUri: string) : Promise<string[]> => {
|
||||||
|
return await putData(`${apiUri}/v2/Job/UpdateAllMetadataJob`, {}) as Promise<string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StartJob = async (apiUri: string, jobId: string, startDependencies: boolean) : Promise<object | undefined> => {
|
||||||
|
return await postData(`${apiUri}/v2/Job/${jobId}/Start`, startDependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StopJob = async (apiUri: string, jobId: string) : Promise<object | undefined> => {
|
||||||
|
return await postData(`${apiUri}/v2/Job/${jobId}/Stop`);
|
||||||
|
}
|
31
tranga-website/src/api/LocalLibrary.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import ILocalLibrary from "./types/ILocalLibrary.ts";
|
||||||
|
import {deleteData, getData, patchData, putData} from "./fetchApi.tsx";
|
||||||
|
import INewLibraryRecord from "./types/records/INewLibraryRecord.ts";
|
||||||
|
|
||||||
|
export const GetLibraries = async (apiUri: string) : Promise<ILocalLibrary[]> => {
|
||||||
|
return await getData(`${apiUri}/v2/LocalLibraries`) as Promise<ILocalLibrary[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetLibrary = async (apiUri: string, libraryId: string) : Promise<ILocalLibrary> => {
|
||||||
|
return await getData(`${apiUri}/v2/LocalLibraries/${libraryId}`) as Promise<ILocalLibrary>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateLibrary = async (apiUri: string, data: INewLibraryRecord) : Promise<ILocalLibrary> => {
|
||||||
|
return await putData(`${apiUri}/v2/LocalLibraries`, data) as Promise<ILocalLibrary>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteLibrary = async (apiUri: string, libraryId: string) : Promise<void> => {
|
||||||
|
return await deleteData(`${apiUri}/v2/LocalLibraries/${libraryId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangeLibraryPath = async (apiUri: string, libraryId: string, newPath: string) : Promise<object | undefined> => {
|
||||||
|
return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}/ChangeBasePath`, newPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangeLibraryName = async (apiUri: string, libraryId: string, newName: string) : Promise<object | undefined> => {
|
||||||
|
return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}/ChangeName`, newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateLibrary = async (apiUri: string, libraryId: string, record: INewLibraryRecord) : Promise<object | undefined> => {
|
||||||
|
return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}`, record);
|
||||||
|
}
|
95
tranga-website/src/api/Manga.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import {deleteData, getData, patchData, postData} from './fetchApi.tsx';
|
||||||
|
import IManga, {DefaultManga} from "./types/IManga.ts";
|
||||||
|
import IChapter from "./types/IChapter.ts";
|
||||||
|
|
||||||
|
export const GetAllManga = async (apiUri: string) : Promise<IManga[]> => {
|
||||||
|
return await getData(`${apiUri}/v2/Manga`) as Promise<IManga[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetMangaWithIds = async (apiUri: string, mangaIds: string[]) : Promise<IManga[]> => {
|
||||||
|
if(mangaIds === undefined || mangaIds === null || mangaIds.length < 1)
|
||||||
|
return Promise.reject("mangaIds was not provided");
|
||||||
|
return await postData(`${apiUri}/v2/Manga/WithIds`, mangaIds) as Promise<IManga[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetMangaById = async (apiUri: string, mangaId: string) : Promise<IManga> => {
|
||||||
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
if(mangaId === DefaultManga.mangaId)
|
||||||
|
return Promise.reject("Default Manga was requested");
|
||||||
|
return await getData(`${apiUri}/v2/Manga/${mangaId}`) as Promise<IManga>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteManga = async (apiUri: string, mangaId: string) : Promise<void> => {
|
||||||
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
if(mangaId === DefaultManga.mangaId)
|
||||||
|
return Promise.reject("Default Manga was requested");
|
||||||
|
return await deleteData(`${apiUri}/v2/Manga/${mangaId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetMangaCoverImageUrl = (apiUri: string, mangaId: string, ref: HTMLImageElement | undefined | null) : string => {
|
||||||
|
if(ref == null || mangaId === DefaultManga.mangaId)
|
||||||
|
return "/blahaj.png";
|
||||||
|
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
|
||||||
|
if(mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
if(mangaId === DefaultManga.mangaId)
|
||||||
|
return Promise.reject("Default Manga was requested");
|
||||||
|
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters`) as Promise<IChapter[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetDownloadedChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
|
||||||
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
if(mangaId === DefaultManga.mangaId)
|
||||||
|
return Promise.reject("Default Manga was requested");
|
||||||
|
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/Downloaded`) as Promise<IChapter[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetNotDownloadedChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
|
||||||
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
if(mangaId === DefaultManga.mangaId)
|
||||||
|
return Promise.reject("Default Manga was requested");
|
||||||
|
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/NotDownloaded`) as Promise<IChapter[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetLatestChapterAvailable = async (apiUri: string, mangaId: string) : Promise<IChapter> => {
|
||||||
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
if(mangaId === DefaultManga.mangaId)
|
||||||
|
return Promise.reject("Default Manga was requested");
|
||||||
|
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestAvailable`) as Promise<IChapter>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetLatestChapterDownloaded = async (apiUri: string, mangaId: string) : Promise<IChapter> => {
|
||||||
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
if(mangaId === DefaultManga.mangaId)
|
||||||
|
return Promise.reject("Default Manga was requested");
|
||||||
|
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestDownloaded`) as Promise<IChapter>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SetIgnoreThreshold = async (apiUri: string, mangaId: string, chapterThreshold: number) : Promise<object | undefined> => {
|
||||||
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
if(chapterThreshold === undefined || chapterThreshold === null)
|
||||||
|
return Promise.reject("chapterThreshold was not provided");
|
||||||
|
if(mangaId === DefaultManga.mangaId)
|
||||||
|
return Promise.reject("Default Manga was requested");
|
||||||
|
return await patchData(`${apiUri}/v2/Manga/${mangaId}/IgnoreChaptersBefore`, chapterThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MoveFolder = async (apiUri: string, mangaId: string, newPath: string) : Promise<object | undefined> => {
|
||||||
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
|
return Promise.reject("mangaId was not provided");
|
||||||
|
if(newPath === undefined || newPath === null || newPath.length < 1)
|
||||||
|
return Promise.reject("newPath was not provided");
|
||||||
|
if(mangaId === DefaultManga.mangaId)
|
||||||
|
return Promise.reject("Default Manga was requested");
|
||||||
|
return await postData(`${apiUri}/v2/Manga/{MangaId}/MoveFolder`, {newPath});
|
||||||
|
}
|
26
tranga-website/src/api/MangaConnector.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {getData, patchData} from './fetchApi.tsx';
|
||||||
|
import IMangaConnector from "./types/IMangaConnector.ts";
|
||||||
|
|
||||||
|
export const GetAllConnectors = async (apiUri: string) : Promise<IMangaConnector[]> => {
|
||||||
|
return await getData(`${apiUri}/v2/MangaConnector`) as Promise<IMangaConnector[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetConnector = async (apiUri: string, mangaConnectorName: string) : Promise<IMangaConnector> => {
|
||||||
|
return await getData(`${apiUri}/v2/MangaConnector/${mangaConnectorName}`) as Promise<IMangaConnector>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetEnabledConnectors = async (apiUri: string) : Promise<IMangaConnector[]> => {
|
||||||
|
return await getData(`${apiUri}/v2/MangaConnector/enabled`) as Promise<IMangaConnector[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetDisabledConnectors = async (apiUri: string) : Promise<IMangaConnector[]> => {
|
||||||
|
return await getData(`${apiUri}/v2/MangaConnector/disabled`) as Promise<IMangaConnector[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SetConnectorEnabled = async (apiUri: string, connectorName: string, enabled: boolean) : Promise<object | undefined> => {
|
||||||
|
if(connectorName === undefined || connectorName === null || connectorName.length < 1)
|
||||||
|
return Promise.reject("connectorName was not provided");
|
||||||
|
if(enabled === undefined || enabled === null)
|
||||||
|
return Promise.reject("enabled was not provided");
|
||||||
|
return await patchData(`${apiUri}/v2/MangaConnector/${connectorName}/SetEnabled/${enabled}`, {});
|
||||||
|
}
|
45
tranga-website/src/api/NotificationConnector.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {deleteData, getData, putData} from "./fetchApi.tsx";
|
||||||
|
import INotificationConnector from "./types/INotificationConnector.ts";
|
||||||
|
import IGotifyRecord from "./types/records/IGotifyRecord.ts";
|
||||||
|
import INtfyRecord from "./types/records/INtfyRecord.ts";
|
||||||
|
import IPushoverRecord from "./types/records/IPushoverRecord.ts";
|
||||||
|
|
||||||
|
export const GetNotificationConnectors = async (apiUri: string) : Promise<INotificationConnector[]> => {
|
||||||
|
return await getData(`${apiUri}/v2/NotificationConnector`) as Promise<INotificationConnector[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateNotificationConnector = async (apiUri: string, newConnector: INotificationConnector) : Promise<string> => {
|
||||||
|
if(newConnector === undefined || newConnector === null)
|
||||||
|
return Promise.reject("newConnector was not provided");
|
||||||
|
return await putData(`${apiUri}/v2/NotificationConnector`, newConnector) as Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetNotificationConnectorWithId = async (apiUri: string, notificationConnectorId: string) : Promise<INotificationConnector> => {
|
||||||
|
if(notificationConnectorId === undefined || notificationConnectorId === null || notificationConnectorId.length < 1)
|
||||||
|
return Promise.reject("notificationConnectorId was not provided");
|
||||||
|
return await getData(`${apiUri}/v2/NotificationConnector/${notificationConnectorId}`) as Promise<INotificationConnector>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteNotificationConnectorWithId = async (apiUri: string, notificationConnectorId: string) : Promise<void> => {
|
||||||
|
if(notificationConnectorId === undefined || notificationConnectorId === null || notificationConnectorId.length < 1)
|
||||||
|
return Promise.reject("notificationConnectorId was not provided");
|
||||||
|
return await deleteData(`${apiUri}/v2/NotificationConnector/${notificationConnectorId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateGotify = async (apiUri: string, gotify: IGotifyRecord) : Promise<string> => {
|
||||||
|
if(gotify === undefined || gotify === null)
|
||||||
|
return Promise.reject("gotify was not provided");
|
||||||
|
return await putData(`${apiUri}/v2/NotificationConnector/Gotify`, gotify) as Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateNtfy = async (apiUri: string, ntfy: INtfyRecord) : Promise<string> => {
|
||||||
|
if(ntfy === undefined || ntfy === null)
|
||||||
|
return Promise.reject("gotify was not provided");
|
||||||
|
return await putData(`${apiUri}/v2/NotificationConnector/Ntfy`, ntfy) as Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreatePushover = async (apiUri: string, pushover: IPushoverRecord) : Promise<string> => {
|
||||||
|
if(pushover === undefined || pushover === null)
|
||||||
|
return Promise.reject("pushover was not provided");
|
||||||
|
return await putData(`${apiUri}/v2/NotificationConnector/Pushover`, pushover) as Promise<string>;
|
||||||
|
}
|
15
tranga-website/src/api/Query.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import IAuthor from "./types/IAuthor.ts";
|
||||||
|
import {getData} from "./fetchApi.tsx";
|
||||||
|
import ILink from "./types/ILink.ts";
|
||||||
|
|
||||||
|
export const GetAuthor = async (apiUri: string, authorId: string) : Promise<IAuthor> => {
|
||||||
|
if(authorId === undefined || authorId === null || authorId.length < 1)
|
||||||
|
return Promise.reject("authorId was not provided");
|
||||||
|
return await getData(`${apiUri}/v2/Query/Author/${authorId}`) as Promise<IAuthor>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetLink = async (apiUri: string, linkId: string) : Promise<ILink> => {
|
||||||
|
if(linkId === undefined || linkId === null || linkId.length < 1)
|
||||||
|
return Promise.reject("linkId was not provided");
|
||||||
|
return await getData(`${apiUri}/v2/Query/Link/${linkId}`) as Promise<ILink>;
|
||||||
|
}
|
22
tranga-website/src/api/Search.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {getData, postData} from "./fetchApi.tsx";
|
||||||
|
import IManga from "./types/IManga.ts";
|
||||||
|
|
||||||
|
export const SearchName = async (apiUri: string, name: string) : Promise<IManga[]> => {
|
||||||
|
if(name === undefined || name === null || name.length < 1)
|
||||||
|
return Promise.reject("name was not provided");
|
||||||
|
return await postData(`${apiUri}/v2/Search/Name`, name) as Promise<IManga[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchNameOnConnector = async (apiUri: string, connectorName: string, name: string) : Promise<IManga[]> => {
|
||||||
|
if(connectorName === undefined || connectorName === null || connectorName.length < 1)
|
||||||
|
return Promise.reject("connectorName was not provided");
|
||||||
|
if(name === undefined || name === null || name.length < 1)
|
||||||
|
return Promise.reject("name was not provided");
|
||||||
|
return await getData(`${apiUri}/v2/Search/${connectorName}/${name}`) as Promise<IManga[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchUrl = async (apiUri: string, url: string) : Promise<IManga> => {
|
||||||
|
if(url === undefined || url === null || url.length < 1)
|
||||||
|
return Promise.reject("name was not provided");
|
||||||
|
return await postData(`${apiUri}/v2/Search/Url`, url) as Promise<IManga>;
|
||||||
|
}
|
89
tranga-website/src/api/fetchApi.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import {createContext} from "react";
|
||||||
|
|
||||||
|
export const ApiUriContext = createContext<string>("");
|
||||||
|
|
||||||
|
export function getData(uri: string) : Promise<object | undefined> {
|
||||||
|
return makeRequestWrapper("GET", uri, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postData(uri: string, content?: object | string | number | boolean | null) : Promise<object | undefined> {
|
||||||
|
return makeRequestWrapper("POST", uri, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteData(uri: string) : Promise<void> {
|
||||||
|
return makeRequestWrapper("DELETE", uri, null) as Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchData(uri: string, content: object | string | number | boolean) : Promise<object | undefined> {
|
||||||
|
return makeRequestWrapper("patch", uri, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putData(uri: string, content: object | string | number | boolean) : Promise<object | undefined> {
|
||||||
|
return makeRequestWrapper("PUT", uri, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRequestWrapper(method: string, uri: string, content?: object | string | number | null | boolean) : Promise<object | undefined>{
|
||||||
|
return makeRequest(method, uri, content)
|
||||||
|
.then((result) => result as Promise<object>)
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn(e);
|
||||||
|
return Promise.reject(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentlyRequestedEndpoints: string[] = [];
|
||||||
|
function makeRequest(method: string, uri: string, content?: object | string | number | null | boolean) : Promise<object | void> {
|
||||||
|
const id = method + uri;
|
||||||
|
if(currentlyRequestedEndpoints.find(x => x == id) != undefined)
|
||||||
|
return Promise.reject(`DO NOT REPORT! Already requested: ${method} ${uri}`);
|
||||||
|
currentlyRequestedEndpoints.push(id);
|
||||||
|
return fetch(uri,
|
||||||
|
{
|
||||||
|
method: method,
|
||||||
|
headers : {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: content ? JSON.stringify(content) : null
|
||||||
|
})
|
||||||
|
.then(function(response){
|
||||||
|
if(!response.ok){
|
||||||
|
if(response.status === 503){
|
||||||
|
currentlyRequestedEndpoints.splice(currentlyRequestedEndpoints.indexOf(id), 1)
|
||||||
|
let retryHeaderVal = response.headers.get("Retry-After");
|
||||||
|
let seconds = 10;
|
||||||
|
if(retryHeaderVal === null){
|
||||||
|
return response.text().then(text => {
|
||||||
|
seconds = parseInt(text);
|
||||||
|
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
||||||
|
.then(() => {
|
||||||
|
return makeRequest(method, uri, content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}else {
|
||||||
|
seconds = parseInt(retryHeaderVal);
|
||||||
|
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
||||||
|
.then(() => {
|
||||||
|
return makeRequest(method, uri, content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}else
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
let json = response.json();
|
||||||
|
return json.then((json) => json).catch(() => null);
|
||||||
|
})
|
||||||
|
.catch(function(err : Error){
|
||||||
|
console.error(`Error ${method}ing Data ${uri}\n${err}`);
|
||||||
|
return Promise.reject();
|
||||||
|
}).finally(() => currentlyRequestedEndpoints.splice(currentlyRequestedEndpoints.indexOf(id), 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidUri(uri: string) : boolean{
|
||||||
|
try {
|
||||||
|
new URL(uri);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
0
tranga-website/src/api/types/EnumLibraryType.ts
Normal file
24
tranga-website/src/api/types/EnumMangaReleaseStatus.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {ColorPaletteProp} from "@mui/joy";
|
||||||
|
|
||||||
|
export enum MangaReleaseStatus {
|
||||||
|
Continuing = "Continuing",
|
||||||
|
Completed = "Completed",
|
||||||
|
OnHiatus = "OnHiatus",
|
||||||
|
Cancelled = "Cancelled",
|
||||||
|
Unreleased = "Unreleased",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReleaseStatusToPalette(status: MangaReleaseStatus): ColorPaletteProp {
|
||||||
|
switch (status) {
|
||||||
|
case MangaReleaseStatus.Continuing:
|
||||||
|
return "success";
|
||||||
|
case MangaReleaseStatus.Completed:
|
||||||
|
return "primary";
|
||||||
|
case MangaReleaseStatus.Cancelled:
|
||||||
|
return "danger";
|
||||||
|
case MangaReleaseStatus.Unreleased:
|
||||||
|
return "neutral";
|
||||||
|
case MangaReleaseStatus.OnHiatus:
|
||||||
|
return "warning";
|
||||||
|
}
|
||||||
|
}
|
8
tranga-website/src/api/types/EnumRequestLimitType.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export enum RequestLimitType {
|
||||||
|
Default = "Default",
|
||||||
|
MangaDexFeed = "MangaDexFeed",
|
||||||
|
MangaImage = "MangaImage",
|
||||||
|
MangaCover = "MangaCover",
|
||||||
|
MangaDexImage = "MangaDexImage",
|
||||||
|
MangaInfo = "MangaInfo"
|
||||||
|
}
|
4
tranga-website/src/api/types/IAuthor.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default interface IAuthor {
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
}
|
19
tranga-website/src/api/types/IBackendSettings.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export default interface IBackendSettings {
|
||||||
|
downloadLocation: string;
|
||||||
|
workingDirectory: string;
|
||||||
|
userAgent: string;
|
||||||
|
aprilFoolsMode: boolean;
|
||||||
|
requestLimits: {
|
||||||
|
Default: number,
|
||||||
|
MangaInfo: number,
|
||||||
|
MangaDexFeed: number,
|
||||||
|
MangaDexImage: number,
|
||||||
|
MangaImage: number,
|
||||||
|
MangaCover: number,
|
||||||
|
};
|
||||||
|
compression: number;
|
||||||
|
bwImages: boolean;
|
||||||
|
startNewJobTimeoutMs: number;
|
||||||
|
chapterNamingScheme: string;
|
||||||
|
flareSolverrUrl: string;
|
||||||
|
}
|
11
tranga-website/src/api/types/IChapter.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export default interface IChapter{
|
||||||
|
chapterId: string;
|
||||||
|
parentMangaId: string;
|
||||||
|
volumeNumber: number | null;
|
||||||
|
chapterNumber: string;
|
||||||
|
url: string;
|
||||||
|
title: string | null;
|
||||||
|
fileName: string | null;
|
||||||
|
downloaded: boolean;
|
||||||
|
fullArchiveFilePath: string;
|
||||||
|
}
|
4
tranga-website/src/api/types/IFrontendSettings.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default interface IFrontendSettings {
|
||||||
|
jobInterval: Date;
|
||||||
|
apiUri: string;
|
||||||
|
}
|
11
tranga-website/src/api/types/ILibraryConnector.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export default interface ILibraryConnector {
|
||||||
|
libraryConnectorId: string;
|
||||||
|
libraryType: LibraryType;
|
||||||
|
baseUrl: string;
|
||||||
|
auth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LibraryType {
|
||||||
|
Komga = "Komga",
|
||||||
|
Kavita = "Kavita"
|
||||||
|
}
|
5
tranga-website/src/api/types/ILink.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default interface ILink {
|
||||||
|
linkId: string;
|
||||||
|
linkProvider: string;
|
||||||
|
linkUrl: string;
|
||||||
|
}
|
5
tranga-website/src/api/types/ILocalLibrary.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default interface ILocalLibrary {
|
||||||
|
localLibraryId: string;
|
||||||
|
basePath: string;
|
||||||
|
libraryName: string;
|
||||||
|
}
|
45
tranga-website/src/api/types/IManga.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {MangaReleaseStatus} from "./EnumMangaReleaseStatus";
|
||||||
|
import IAuthor from "./IAuthor.ts";
|
||||||
|
import IMangaAltTitle from "./IMangaAltTitle.ts";
|
||||||
|
import IMangaTag from "./IMangaTag.ts";
|
||||||
|
import ILink from "./ILink.ts";
|
||||||
|
|
||||||
|
export default interface IManga{
|
||||||
|
mangaId: string;
|
||||||
|
idOnConnectorSite: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
websiteUrl: string;
|
||||||
|
releaseStatus: MangaReleaseStatus;
|
||||||
|
libraryId: string | null;
|
||||||
|
mangaConnectorName: string;
|
||||||
|
authors: IAuthor[] | null;
|
||||||
|
mangaTags: IMangaTag[] | null;
|
||||||
|
links: ILink[] | null;
|
||||||
|
altTitles: IMangaAltTitle[] | null;
|
||||||
|
ignoreChaptersBefore: number;
|
||||||
|
directoryName: string;
|
||||||
|
year: number | null;
|
||||||
|
originalLanguage: string | null;
|
||||||
|
chapterIds: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultManga : IManga = {
|
||||||
|
mangaId: "Loading",
|
||||||
|
idOnConnectorSite: "Loading",
|
||||||
|
name: "Loading",
|
||||||
|
description: "Loading",
|
||||||
|
websiteUrl: "",
|
||||||
|
releaseStatus: MangaReleaseStatus.Continuing,
|
||||||
|
libraryId: null,
|
||||||
|
mangaConnectorName: "Loading",
|
||||||
|
authors: null,
|
||||||
|
mangaTags: null,
|
||||||
|
links: null,
|
||||||
|
altTitles: null,
|
||||||
|
ignoreChaptersBefore: 0,
|
||||||
|
directoryName: "",
|
||||||
|
year: 1999,
|
||||||
|
originalLanguage: "en",
|
||||||
|
chapterIds: null
|
||||||
|
}
|
5
tranga-website/src/api/types/IMangaAltTitle.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default interface IMangaAltTitle {
|
||||||
|
altTitleId: string;
|
||||||
|
language: string;
|
||||||
|
title: string;
|
||||||
|
}
|
7
tranga-website/src/api/types/IMangaConnector.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default interface IMangaConnector {
|
||||||
|
name: string;
|
||||||
|
supportedLanguages: string[];
|
||||||
|
iconUrl: string;
|
||||||
|
baseUris: string[];
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
3
tranga-website/src/api/types/IMangaTag.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default interface IMangaTag {
|
||||||
|
tag: string;
|
||||||
|
}
|
7
tranga-website/src/api/types/INotificationConnector.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default interface INotificationConnector {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
headers: Record<string, string>[];
|
||||||
|
httpMethod: string;
|
||||||
|
body: string;
|
||||||
|
}
|
8
tranga-website/src/api/types/IRequestLimits.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default interface IRequestLimits {
|
||||||
|
Default: number;
|
||||||
|
MangaDexFeed: number;
|
||||||
|
MangaImage: number;
|
||||||
|
MangaCover: number;
|
||||||
|
MangaDexImage: number;
|
||||||
|
MangaInfo: number;
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import IJobWithMangaId from "./IJobWithMangaId.ts";
|
||||||
|
|
||||||
|
export default interface IDownloadAvailableChaptersJob extends IJobWithMangaId {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import IJobWithMangaId from "./IJobWithMangaId.ts";
|
||||||
|
|
||||||
|
export default interface IDownloadMangaCoverJob extends IJobWithMangaId {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import IJobWithChapterId from "./IJobWithChapterId.tsx";
|
||||||
|
|
||||||
|
export default interface IDownloadSingleChapterJob extends IJobWithChapterId {
|
||||||
|
|
||||||
|
}
|
36
tranga-website/src/api/types/Jobs/IJob.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export default interface IJob{
|
||||||
|
jobId: string;
|
||||||
|
parentJobId: string | null;
|
||||||
|
jobType: JobType;
|
||||||
|
recurrenceMs: number;
|
||||||
|
lastExecution: Date;
|
||||||
|
nextExecution: Date;
|
||||||
|
state: JobState;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum JobType {
|
||||||
|
DownloadSingleChapterJob = "DownloadSingleChapterJob",
|
||||||
|
DownloadAvailableChaptersJob = "DownloadAvailableChaptersJob",
|
||||||
|
DownloadMangaCoverJob = "DownloadMangaCoverJob",
|
||||||
|
RetrieveChaptersJob = "RetrieveChaptersJob",
|
||||||
|
UpdateChaptersDownloadedJob = "UpdateChaptersDownloadedJob",
|
||||||
|
MoveMangaLibraryJob = "MoveMangaLibraryJob",
|
||||||
|
UpdateCoverJob = "UpdateCoverJob"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JobTypeToString(job: JobType | string): string {
|
||||||
|
return job.replace(/([A-Z])/g, ' $1').replace("Job", "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum JobState {
|
||||||
|
FirstExecution = "FirstExecution",
|
||||||
|
Running = "Running",
|
||||||
|
Completed = "Completed",
|
||||||
|
CompletedWaiting = "CompletedWaiting",
|
||||||
|
Failed = "Failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JobStateToString(state: JobState | string): string {
|
||||||
|
return state.replace(/([A-Z])/g, ' $1').trim();
|
||||||
|
}
|
5
tranga-website/src/api/types/Jobs/IJobWithChapterId.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import IJob from "./IJob.ts";
|
||||||
|
|
||||||
|
export default interface IJobWithChapterId extends IJob {
|
||||||
|
chapterId: string;
|
||||||
|
}
|
5
tranga-website/src/api/types/Jobs/IJobWithMangaId.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import IJob from "./IJob.ts";
|
||||||
|
|
||||||
|
export default interface IJobWithMangaId extends IJob {
|
||||||
|
mangaId: string;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import IJob from "./IJob";
|
||||||
|
|
||||||
|
export default interface IMoveFileOrFolderJob extends IJob {
|
||||||
|
fromLocation: string;
|
||||||
|
toLocation: string;
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import IJobWithMangaId from "./IJobWithMangaId.ts";
|
||||||
|
|
||||||
|
export default interface IMoveMangaLibraryJob extends IJobWithMangaId {
|
||||||
|
ToLibraryId: string;
|
||||||
|
}
|