Compare commits
111 Commits
a59920f09d
...
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 | |||
594103689e | |||
6ab431fde0 | |||
18257cd584 | |||
c6dc3cd107 | |||
94c47b904d | |||
2a4316cf1d | |||
89586ef891 | |||
33514ee375 | |||
526e129fb7 | |||
d1dae83387 | |||
9199a7a0e4 | |||
d9fa4185e2 | |||
717c8cd33e | |||
483c2d564b | |||
5807844281 | |||
77d9e6eee1 | |||
e45e7bd5f5 | |||
ccab3a8027 | |||
5eca8dac5d | |||
0b34ca7723 | |||
eb943ccbed | |||
514cd06d2b | |||
2035bc8f4e | |||
ece175d1db | |||
da3867962b | |||
ec15732e57 | |||
6d10c81dff | |||
a3842ba20f | |||
74b71d57b7 | |||
6a5e340882 | |||
95801828c6 | |||
b70dfbf482 | |||
0bc93a7e5c | |||
ddd9512469 | |||
c26e208822 | |||
c656e5face | |||
4addacb229 | |||
c1aece8200 | |||
6bd9a0b1db | |||
c72e152a7e | |||
38572a3a46 | |||
5b52577610 | |||
3b99dbd487 | |||
3eb7b63fa6 | |||
793561dad7 | |||
acb22d770d | |||
e0093c65ff | |||
ecf9e1a243 | |||
5af3005179 | |||
d33ac16c7d | |||
a2e0a2375a | |||
9c0a7a0a50 | |||
d05b8ea76b | |||
e9937c02bf | |||
ef5524d7b6 | |||
8f9700ec02 | |||
4b86cd7104 | |||
61e3fbd500 | |||
657a3d1857 | |||
b4cf22b6eb | |||
2216e13c49 | |||
0a68da2b49 | |||
b87c159823 |
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@v3.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@v3.6.1
|
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@v6.7.0
|
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: |
|
||||||
|
10
.github/workflows/docker-image-master.yml
vendored
@ -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@v3.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@v3.6.1
|
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 +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@v6.7.0
|
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: |
|
||||||
|
@ -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@v3.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@v3.6.1
|
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 +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@v6.7.0
|
uses: docker/build-push-action@v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
@ -12,6 +12,8 @@ FROM nginx:alpine3.17-slim
|
|||||||
# Copy built files from Vite's dist folder
|
# Copy built files from Vite's dist folder
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
#COPY --from=builder /app/tranga-website/media /usr/share/nginx/html/media
|
#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;"]
|
16
README.md
@ -33,19 +33,9 @@
|
|||||||
<!-- ABOUT THE PROJECT -->
|
<!-- ABOUT THE PROJECT -->
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
| Default View | Search Window | Search Results |
|
|  |  |  |
|
||||||
|-------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
|
||||||
|  | <br/> | <br/> |
|
|  |  |  |
|
||||||
| Search opens with click on "Add new Manga".<br/>Settings are on the top right | When selecting different connectors, available languages automatically update.<br/>Spinners to indicate action being performed | Clicking on an Item here will bring up a view with more information |
|
|
||||||
|
|
||||||
| | Different Views for Manga | |
|
|
||||||
|----------------------------------------------------------------------------|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
|
|
||||||
|  |  |  |
|
|
||||||
|
|
||||||
| | Settings Dialog | |
|
|
||||||
|-|----------------------------------------------------------------------------|-|
|
|
||||||
| |  | |
|
|
||||||
|
|
||||||
|
|
||||||
## About The Project
|
## About The Project
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 149 KiB |
Before Width: | Height: | Size: 150 KiB |
Before Width: | Height: | Size: 376 KiB |
Before Width: | Height: | Size: 406 KiB |
Before Width: | Height: | Size: 508 KiB |
Before Width: | Height: | Size: 539 KiB |
Before Width: | Height: | Size: 249 KiB |
Before Width: | Height: | Size: 192 KiB |
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 |
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;
|
12
tranga-website/package-lock.json
generated
@ -2305,9 +2305,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -2560,9 +2560,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: calc(100% - 60px);
|
|
||||||
top: 60px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
@ -2,30 +2,77 @@ import Sheet from '@mui/joy/Sheet';
|
|||||||
import './App.css'
|
import './App.css'
|
||||||
import Settings from "./Settings.tsx";
|
import Settings from "./Settings.tsx";
|
||||||
import Header from "./Header.tsx";
|
import Header from "./Header.tsx";
|
||||||
import {Badge, Box, Button, Card, CardContent, CardCover, Typography} from "@mui/joy";
|
import {Badge, Button} from "@mui/joy";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {ApiUriContext} from "./api/fetchApi.tsx";
|
import {ApiUriContext} from "./api/fetchApi.tsx";
|
||||||
import Search from './Components/Search.tsx';
|
import Search from './Components/Search.tsx';
|
||||||
import MangaList from "./Components/MangaList.tsx";
|
import MangaList from "./Components/MangaList.tsx";
|
||||||
import {CardHeight, CardWidth} from "./Components/Manga.tsx";
|
|
||||||
import {MangaConnectorContext} from "./api/Contexts/MangaConnectorContext.tsx";
|
import {MangaConnectorContext} from "./api/Contexts/MangaConnectorContext.tsx";
|
||||||
import IMangaConnector from "./api/types/IMangaConnector.ts";
|
import IMangaConnector from "./api/types/IMangaConnector.ts";
|
||||||
import {GetAllConnectors} from "./api/MangaConnector.tsx";
|
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 () {
|
export default function App () {
|
||||||
|
|
||||||
const [showSettings, setShowSettings] = useState<boolean>(false);
|
const [showSettings, setShowSettings] = useState<boolean>(false);
|
||||||
const [showSearch, setShowSearch] = useState<boolean>(false);
|
const [showSearch, setShowSearch] = useState<boolean>(false);
|
||||||
|
const [showJobs, setShowJobs] = useState<boolean>(false);
|
||||||
const [apiConnected, setApiConnected] = useState<boolean>(false);
|
const [apiConnected, setApiConnected] = useState<boolean>(false);
|
||||||
|
|
||||||
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/"));
|
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/api";
|
||||||
|
|
||||||
const [apiUri, setApiUri] = useState<string>(apiUriStr);
|
const [apiUri, setApiUri] = useState<string>(apiUriStr);
|
||||||
|
const [mangas, setMangas] = useState<IManga[]>([]);
|
||||||
|
const [chapters, setChapters] = useState<IChapter[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("apiUri", apiUri);
|
localStorage.setItem("apiUri", 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[]>([]);
|
const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -35,38 +82,26 @@ export default function App () {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ApiUriContext.Provider value={apiUri}>
|
<ApiUriContext.Provider value={apiUri}>
|
||||||
<MangaConnectorContext value={mangaConnectors}>
|
<MangaConnectorContext.Provider value={mangaConnectors}>
|
||||||
<Sheet className={"app"}>
|
<MangaContext.Provider value={{mangas, GetManga}}>
|
||||||
<Header>
|
<ChapterContext.Provider value={{chapters, GetChapter}}>
|
||||||
<Badge color={"danger"} invisible={apiConnected} badgeContent={"!"}>
|
<Sheet className={"app"}>
|
||||||
<Button onClick={() => setShowSettings(true)}>Settings</Button>
|
<Header>
|
||||||
</Badge>
|
<Badge color={"danger"} invisible={apiConnected} badgeContent={"!"}>
|
||||||
</Header>
|
<Button onClick={() => setShowSettings(true)}>Settings</Button>
|
||||||
<Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri} setConnected={setApiConnected} />
|
</Badge>
|
||||||
<Search open={showSearch} setOpen={setShowSearch} />
|
<Button onClick={() => setShowJobs(true)}>Jobs</Button>
|
||||||
<Sheet className={"app-content"}>
|
</Header>
|
||||||
<MangaList connected={apiConnected}>
|
<Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri} setConnected={setApiConnected} />
|
||||||
<Badge invisible sx={{margin: "8px !important"}}>
|
<Search open={showSearch} setOpen={setShowSearch} />
|
||||||
<Card onClick={() => setShowSearch(true)} sx={{height:"fit-content",width:"fit-content"}}>
|
<JobsDrawer open={showJobs} connected={apiConnected} setOpen={setShowJobs} />
|
||||||
<CardCover sx={{margin:"var(--Card-padding)"}}>
|
<Sheet className={"app-content"}>
|
||||||
<img src={"/blahaj.png"} style={{height: CardHeight + "px", width: CardWidth + 10 + "px"}} />
|
<MangaList connected={apiConnected} setShowSearch={setShowSearch} />
|
||||||
</CardCover>
|
</Sheet>
|
||||||
<CardCover sx={{
|
</Sheet>
|
||||||
background: 'rgba(234, 119, 246, 0.14)',
|
</ChapterContext.Provider>
|
||||||
backdropFilter: 'blur(6.9px)',
|
</MangaContext.Provider>
|
||||||
webkitBackdropFilter: 'blur(6.9px)',
|
</MangaConnectorContext.Provider>
|
||||||
}}/>
|
|
||||||
<CardContent>
|
|
||||||
<Box style={{height: CardHeight + "px", width: CardWidth + 10 + "px"}} >
|
|
||||||
<Typography level={"h1"}>Search</Typography>
|
|
||||||
</Box>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Badge>
|
|
||||||
</MangaList>
|
|
||||||
</Sheet>
|
|
||||||
</Sheet>
|
|
||||||
</MangaConnectorContext>
|
|
||||||
</ApiUriContext.Provider>
|
</ApiUriContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import {Chip, ColorPaletteProp} from "@mui/joy";
|
|
||||||
import IAuthor from "../api/types/IAuthor.ts";
|
|
||||||
|
|
||||||
export default function AuthorTag({author, color} : {author: IAuthor, color?: ColorPaletteProp }) {
|
|
||||||
return (
|
|
||||||
<Chip variant={"outlined"} size={"md"} color={color??"primary"}>
|
|
||||||
{author.authorName ?? "Load Failed"}
|
|
||||||
</Chip>
|
|
||||||
);
|
|
||||||
}
|
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +0,0 @@
|
|||||||
import {Chip, Link, ColorPaletteProp} from "@mui/joy";
|
|
||||||
import ILink from "../api/types/ILink.ts";
|
|
||||||
|
|
||||||
export default function LinkTag({link, color} : { link: ILink | undefined, color?: ColorPaletteProp }) {
|
|
||||||
return (
|
|
||||||
<Chip variant={"soft"} size={"sm"} color={color??"primary"}>
|
|
||||||
<Link sx={{textDecoration:"underline"}} level={"body-xs"} href={link?.linkUrl}>{link?.linkProvider??"Load Failed"}</Link>
|
|
||||||
</Chip>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,42 +1,64 @@
|
|||||||
import {
|
import {Badge, Box, Card, CardContent, CardCover, Skeleton, Tooltip, Typography,} from "@mui/joy";
|
||||||
Badge,
|
|
||||||
Box,
|
|
||||||
Card,
|
|
||||||
CardContent, CardCover,
|
|
||||||
Link,
|
|
||||||
} from "@mui/joy";
|
|
||||||
import IManga from "../api/types/IManga.ts";
|
import IManga from "../api/types/IManga.ts";
|
||||||
import {CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
|
import {CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
|
||||||
import {GetMangaById, GetMangaCoverImageUrl} from "../api/Manga.tsx";
|
import {GetMangaCoverImageUrl} from "../api/Manga.tsx";
|
||||||
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
|
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
|
||||||
import {ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
|
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
|
||||||
import {SxProps} from "@mui/joy/styles/types";
|
import {SxProps} from "@mui/joy/styles/types";
|
||||||
import MangaPopup from "./MangaPopup.tsx";
|
import MangaPopup from "./MangaPopup.tsx";
|
||||||
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx";
|
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx";
|
||||||
|
import {MangaContext} from "../api/Contexts/MangaContext.tsx";
|
||||||
export function MangaFromId({mangaId, children} : { mangaId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
|
|
||||||
const [manga, setManga] = useState<IManga>();
|
|
||||||
|
|
||||||
const apiUri = useContext(ApiUriContext);
|
|
||||||
|
|
||||||
const loadManga = useCallback(() => {
|
|
||||||
GetMangaById(apiUri, mangaId).then(setManga);
|
|
||||||
},[apiUri, mangaId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadManga();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{manga === undefined ? <></> : <Manga manga={manga} children={children} /> }
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CardWidth = 190;
|
export const CardWidth = 190;
|
||||||
export const CardHeight = 300;
|
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}) {
|
export function Manga({manga: manga, children} : { manga: IManga, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
|
||||||
const CoverRef = useRef<HTMLImageElement>(null);
|
const CoverRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
@ -60,34 +82,24 @@ export function Manga({manga: manga, children} : { manga: IManga, children?: Rea
|
|||||||
getData(coverUrl).then(() => {
|
getData(coverUrl).then(() => {
|
||||||
if(CoverRef.current) CoverRef.current.src = coverUrl;
|
if(CoverRef.current) CoverRef.current.src = coverUrl;
|
||||||
});
|
});
|
||||||
}, [manga, apiUri])
|
}, [manga, apiUri]);
|
||||||
|
|
||||||
const coverSx : SxProps = {
|
|
||||||
height: CardHeight + "px",
|
|
||||||
width: CardWidth + "px",
|
|
||||||
position: "relative",
|
|
||||||
}
|
|
||||||
|
|
||||||
const coverCss : CSSProperties = {
|
|
||||||
maxHeight: "calc("+CardHeight+"px + 2rem)",
|
|
||||||
maxWidth: "calc("+CardWidth+"px + 2rem)",
|
|
||||||
}
|
|
||||||
|
|
||||||
const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"];
|
const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"];
|
||||||
|
|
||||||
const mangaName = manga.name.length > 30 ? manga.name.substring(0, 27) + "..." : manga.name;
|
const maxLength = 50;
|
||||||
|
const mangaName = manga.name.length > maxLength ? manga.name.substring(0, maxLength-3) + "..." : manga.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge sx={{margin:"8px !important"}} badgeContent={mangaConnector ? <img width={"24pt"} height={"24pt"} src={mangaConnector.iconUrl} /> : manga.mangaConnectorName} color={ReleaseStatusToPalette(manga.releaseStatus)} size={"lg"}>
|
<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) => {
|
<Card sx={{height:"fit-content",width:"fit-content"}} onClick={(e) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if(interactiveElements.find(x => x == target.localName) == undefined)
|
if(interactiveElements.find(x => x == target.localName) == undefined)
|
||||||
setExpanded(!expanded)}
|
setExpanded(!expanded)}
|
||||||
}>
|
}>
|
||||||
<CardCover>
|
<CardCover>
|
||||||
<img style={coverCss} src={GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current)} alt="Manga Cover"
|
<img loading={"lazy"} style={coverCss} src={GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current)} alt="Manga Cover"
|
||||||
ref={CoverRef}
|
ref={CoverRef}
|
||||||
onLoad={LoadMangaCover}/>
|
onError={LoadMangaCover}/>
|
||||||
</CardCover>
|
</CardCover>
|
||||||
<CardCover sx={{
|
<CardCover sx={{
|
||||||
background:
|
background:
|
||||||
@ -95,12 +107,12 @@ export function Manga({manga: manga, children} : { manga: IManga, children?: Rea
|
|||||||
}}/>
|
}}/>
|
||||||
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
|
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
|
||||||
<Box sx={coverSx}>
|
<Box sx={coverSx}>
|
||||||
<Link href={manga.websiteUrl} level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
|
<Typography level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
|
||||||
{mangaName}
|
{mangaName}
|
||||||
</Link>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<MangaPopup manga={manga} open={expanded}>{children}</MangaPopup>
|
<MangaPopup manga={manga} open={expanded} setOpen={setExpanded}>{children}</MangaPopup>
|
||||||
</Card>
|
</Card>
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import {Button, Stack} from "@mui/joy";
|
import {Badge, Box, Button, Card, CardContent, CardCover, Stack, Tooltip, Typography} from "@mui/joy";
|
||||||
import {useCallback, useContext, useEffect, useState} from "react";
|
import {Dispatch, SetStateAction, useCallback, useContext, useEffect, useState} from "react";
|
||||||
import {ApiUriContext} from "../api/fetchApi.tsx";
|
import {ApiUriContext} from "../api/fetchApi.tsx";
|
||||||
import {DeleteJob, GetJobsWithType} from "../api/Job.tsx";
|
import {DeleteJob, GetJobsWithType, StartJob} from "../api/Job.tsx";
|
||||||
import {JobType} from "../api/types/Jobs/IJob.ts";
|
import {JobType} from "../api/types/Jobs/IJob.ts";
|
||||||
import IDownloadAvailableChaptersJob from "../api/types/Jobs/IDownloadAvailableChaptersJob.ts";
|
import IDownloadAvailableChaptersJob from "../api/types/Jobs/IDownloadAvailableChaptersJob.ts";
|
||||||
import {MangaFromId} from "./Manga.tsx";
|
import {CardHeight, CardWidth, MangaFromId} from "./Manga.tsx";
|
||||||
import { Remove } from "@mui/icons-material";
|
import {PlayArrow, Remove} from "@mui/icons-material";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
export default function MangaList({connected, children}: {connected: boolean, children?: React.ReactNode} ){
|
export default function MangaList({connected, setShowSearch}: {connected: boolean, setShowSearch: Dispatch<SetStateAction<boolean>>} ) {
|
||||||
const apiUri = useContext(ApiUriContext);
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
const [jobList, setJobList] = useState<IDownloadAvailableChaptersJob[]>([]);
|
const [jobList, setJobList] = useState<IDownloadAvailableChaptersJob[]>([]);
|
||||||
@ -23,17 +23,16 @@ export default function MangaList({connected, children}: {connected: boolean, ch
|
|||||||
DeleteJob(apiUri, jobId).finally(() => getJobList());
|
DeleteJob(apiUri, jobId).finally(() => getJobList());
|
||||||
},[apiUri]);
|
},[apiUri]);
|
||||||
|
|
||||||
useEffect(() => {
|
const startJob = useCallback((jobId: string) => {
|
||||||
getJobList();
|
StartJob(apiUri, jobId, true).finally(() => getJobList());
|
||||||
}, [apiUri]);
|
},[apiUri]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateTimer();
|
updateTimer();
|
||||||
getJobList();
|
}, [connected, apiUri]);
|
||||||
}, [connected]);
|
|
||||||
|
|
||||||
const timerRef = React.useRef<ReturnType<typeof setInterval>>(undefined);
|
const timerRef = React.useRef<ReturnType<typeof setInterval>>(undefined);
|
||||||
const updateTimer = () => {
|
const updateTimer = useCallback(() => {
|
||||||
if(!connected){
|
if(!connected){
|
||||||
clearTimeout(timerRef.current);
|
clearTimeout(timerRef.current);
|
||||||
return;
|
return;
|
||||||
@ -41,18 +40,35 @@ export default function MangaList({connected, children}: {connected: boolean, ch
|
|||||||
if(timerRef.current === undefined) {
|
if(timerRef.current === undefined) {
|
||||||
console.log("Added timer!");
|
console.log("Added timer!");
|
||||||
getJobList();
|
getJobList();
|
||||||
timerRef.current = setInterval(() => {
|
timerRef.current = setInterval(getJobList, 2000);
|
||||||
getJobList();
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [getJobList, connected, timerRef]);
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<Stack direction="row" spacing={1} flexWrap={"wrap"}>
|
<Stack direction="row" spacing={1} flexWrap={"wrap"} sx={{overflowX: 'hidden', overflowY: 'auto' /* Badge overflow */}} paddingTop={"6px" /* Badge overflow */}>
|
||||||
{children}
|
<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) => (
|
{jobList?.map((job) => (
|
||||||
<MangaFromId key={job.mangaId} mangaId={job.mangaId}>
|
<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>
|
<Button color={"danger"} endDecorator={<Remove />} onClick={() => deleteJob(job.jobId)}>Delete</Button>
|
||||||
</MangaFromId>
|
</MangaFromId>
|
||||||
))}
|
))}
|
||||||
|
@ -1,18 +1,70 @@
|
|||||||
import IManga from "../api/types/IManga.ts";
|
import IManga from "../api/types/IManga.ts";
|
||||||
import {Badge, Box, Chip, CircularProgress, Drawer, Input, Skeleton, Stack, Typography} from "@mui/joy";
|
import {Badge, Box, Chip, CircularProgress, Drawer, Input, Link, Skeleton, Stack, Typography} from "@mui/joy";
|
||||||
import {ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
|
import React, {ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
|
||||||
import {GetLatestChapterAvailable, GetMangaCoverImageUrl, SetIgnoreThreshold} from "../api/Manga.tsx";
|
import {
|
||||||
|
GetLatestChapterAvailable,
|
||||||
|
GetLatestChapterDownloaded,
|
||||||
|
GetMangaCoverImageUrl,
|
||||||
|
SetIgnoreThreshold
|
||||||
|
} from "../api/Manga.tsx";
|
||||||
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
|
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
|
||||||
import AuthorTag from "./AuthorTag.tsx";
|
|
||||||
import LinkTag from "./LinkTag.tsx";
|
|
||||||
import MarkdownPreview from "@uiw/react-markdown-preview";
|
import MarkdownPreview from "@uiw/react-markdown-preview";
|
||||||
import {CardHeight} from "./Manga.tsx";
|
import {CardHeight} from "./Manga.tsx";
|
||||||
import IChapter from "../api/types/IChapter.ts";
|
import IChapter from "../api/types/IChapter.ts";
|
||||||
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
|
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
|
||||||
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx";
|
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx";
|
||||||
|
import {MangaContext} from "../api/Contexts/MangaContext.tsx";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
|
||||||
|
|
||||||
export default function MangaPopup({manga, open, children} : {manga: IManga | null, open: boolean, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
|
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 apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
@ -21,6 +73,8 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
|
|||||||
const LoadMangaCover = useCallback(() => {
|
const LoadMangaCover = useCallback(() => {
|
||||||
if(CoverRef.current == null || manga == null)
|
if(CoverRef.current == null || manga == null)
|
||||||
return;
|
return;
|
||||||
|
if (!open)
|
||||||
|
return;
|
||||||
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
|
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
|
||||||
if(CoverRef.current.src == coverUrl)
|
if(CoverRef.current.src == coverUrl)
|
||||||
return;
|
return;
|
||||||
@ -29,12 +83,13 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
|
|||||||
getData(coverUrl).then(() => {
|
getData(coverUrl).then(() => {
|
||||||
if(CoverRef.current) CoverRef.current.src = coverUrl;
|
if(CoverRef.current) CoverRef.current.src = coverUrl;
|
||||||
});
|
});
|
||||||
}, [manga, apiUri])
|
}, [manga, apiUri, open])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(!open)
|
if(!open)
|
||||||
return;
|
return;
|
||||||
LoadMaxChapter();
|
LoadMaxChapter();
|
||||||
|
LoadDownloadedChapter();
|
||||||
LoadMangaCover();
|
LoadMangaCover();
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
@ -49,8 +104,19 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
|
|||||||
.finally(() => setMaxChapterLoading(false));
|
.finally(() => setMaxChapterLoading(false));
|
||||||
}, [manga, apiUri]);
|
}, [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 [updatingThreshold, setUpdatingThreshold] = useState<boolean>(false);
|
||||||
const updateIgnoreThreshhold = useCallback((value: number) => {
|
const updateIgnoreThreshold = useCallback((value: number) => {
|
||||||
if(manga == null)
|
if(manga == null)
|
||||||
return;
|
return;
|
||||||
setUpdatingThreshold(true);
|
setUpdatingThreshold(true);
|
||||||
@ -60,7 +126,8 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
|
|||||||
const mangaConnector = useContext(MangaConnectorContext).find(all => all.name == manga?.mangaConnectorName);
|
const mangaConnector = useContext(MangaConnectorContext).find(all => all.name == manga?.mangaConnectorName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer anchor="bottom" size="lg" open={open}>
|
<Drawer anchor="bottom" size="lg" open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalClose />
|
||||||
<Stack direction="column" spacing={2} margin={"10px"}>
|
<Stack direction="column" spacing={2} margin={"10px"}>
|
||||||
{ /* Cover and Description */ }
|
{ /* Cover and Description */ }
|
||||||
<Stack direction="row" spacing={2} margin={"10px"}>
|
<Stack direction="row" spacing={2} margin={"10px"}>
|
||||||
@ -70,11 +137,17 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
|
|||||||
onLoad={LoadMangaCover}/>
|
onLoad={LoadMangaCover}/>
|
||||||
</Badge>
|
</Badge>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography level={"h2"} marginTop={"20px"}>{manga?.name}</Typography>
|
<Link target={"_blank"} href={manga?.websiteUrl} level={"h2"}>
|
||||||
<Stack direction={"row"} flexWrap={"wrap"} spacing={0.5} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
|
{manga?.name}
|
||||||
{manga?.authors?.map(author => <AuthorTag key={author.authorId} author={author} color={"success"} />)}
|
</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?.mangaTags?.map(tag => <Chip key={tag.tag} variant={"soft"} size={"md"} color={"primary"}>{tag.tag}</Chip>)}
|
||||||
{manga?.links?.map(link => <LinkTag key={link.linkId} link={link} color={"warning"} />)}
|
{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>
|
</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"}} />
|
<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>
|
</Box>
|
||||||
@ -84,7 +157,7 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
|
|||||||
<Stack direction="row" spacing={2}>
|
<Stack direction="row" spacing={2}>
|
||||||
<Input
|
<Input
|
||||||
type={"number"}
|
type={"number"}
|
||||||
placeholder={"0.0"}
|
placeholder={downloadedChapterLoading ? "" : mangaDownloadedChapter?.chapterNumber??"0.0"}
|
||||||
startDecorator={
|
startDecorator={
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
@ -103,7 +176,7 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
|
|||||||
}
|
}
|
||||||
sx={{width:"min-content"}}
|
sx={{width:"min-content"}}
|
||||||
size={"md"}
|
size={"md"}
|
||||||
onChange={(e) => updateIgnoreThreshhold(e.currentTarget.valueAsNumber)}
|
onChange={(e) => updateIgnoreThreshold(e.currentTarget.valueAsNumber)}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -21,7 +21,7 @@ import {useCallback, useContext, useEffect, useState} from "react";
|
|||||||
import {ApiUriContext} from "../api/fetchApi.tsx";
|
import {ApiUriContext} from "../api/fetchApi.tsx";
|
||||||
import {GetAllConnectors} from "../api/MangaConnector.tsx";
|
import {GetAllConnectors} from "../api/MangaConnector.tsx";
|
||||||
import IManga from "../api/types/IManga.ts";
|
import IManga from "../api/types/IManga.ts";
|
||||||
import {SearchNameOnConnector} from "../api/Search.tsx";
|
import {SearchNameOnConnector, SearchUrl} from "../api/Search.tsx";
|
||||||
import {Manga} from "./Manga.tsx";
|
import {Manga} from "./Manga.tsx";
|
||||||
import Add from "@mui/icons-material/Add";
|
import Add from "@mui/icons-material/Add";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -32,7 +32,7 @@ import { LibraryBooks } from "@mui/icons-material";
|
|||||||
|
|
||||||
export default function Search({open, setOpen}:{open:boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>}){
|
export default function Search({open, setOpen}:{open:boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>}){
|
||||||
|
|
||||||
const [step, setStep] = useState<number>(1);
|
const [step, setStep] = useState<number>(2);
|
||||||
|
|
||||||
const apiUri = useContext(ApiUriContext);
|
const apiUri = useContext(ApiUriContext);
|
||||||
const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>();
|
const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>();
|
||||||
@ -48,14 +48,28 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
|||||||
const [resultsLoading, setResultsLoading] = useState<boolean>(false);
|
const [resultsLoading, setResultsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const StartSearch = useCallback((mangaConnector : IMangaConnector | undefined, value: string)=>{
|
const StartSearch = useCallback((mangaConnector : IMangaConnector | undefined, value: string)=>{
|
||||||
setStep(3);
|
if(mangaConnector === undefined && !IsValidUrl(value))
|
||||||
if(mangaConnector === undefined)
|
|
||||||
return;
|
return;
|
||||||
setResults([]);
|
setResults(undefined);
|
||||||
setResultsLoading(true);
|
setResultsLoading(true);
|
||||||
SearchNameOnConnector(apiUri, mangaConnector.name, value).then(setResults).finally(() => setResultsLoading(false));
|
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])
|
},[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 [localLibraries, setLocalLibraries] = useState<ILocalLibrary[]>();
|
||||||
const [localLibrariesLoading, setLocalLibrariesLoading] = useState<boolean>(true);
|
const [localLibrariesLoading, setLocalLibrariesLoading] = useState<boolean>(true);
|
||||||
const [selectedLibraryId, setSelectedLibraryId] = useState<string>();
|
const [selectedLibraryId, setSelectedLibraryId] = useState<string>();
|
||||||
@ -101,12 +115,12 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
|||||||
<ModalClose />
|
<ModalClose />
|
||||||
<Stepper orientation={"vertical"} sx={{ height: '100%', width: "calc(100% - 80px)", margin:"40px"}}>
|
<Stepper orientation={"vertical"} sx={{ height: '100%', width: "calc(100% - 80px)", margin:"40px"}}>
|
||||||
<Step indicator={
|
<Step indicator={
|
||||||
<StepIndicator variant={step==1?"solid":"outlined"} color={mangaConnectors?.length??0 < 1 ? "danger" : "primary"}>
|
<StepIndicator variant={step==1?"solid":"outlined"} color={(mangaConnectors?.length??0) < 1 ? "danger" : "primary"}>
|
||||||
1
|
1
|
||||||
</StepIndicator>}>
|
</StepIndicator>}>
|
||||||
<Skeleton loading={mangaConnectorsLoading}>
|
<Skeleton loading={mangaConnectorsLoading}>
|
||||||
<Select
|
<Select
|
||||||
color={mangaConnectors?.length??0 < 1 ? "danger" : "neutral"}
|
color={(mangaConnectors?.length??0) < 1 ? "danger" : "neutral"}
|
||||||
disabled={mangaConnectorsLoading || resultsLoading || mangaConnectors?.length == null || mangaConnectors.length < 1}
|
disabled={mangaConnectorsLoading || resultsLoading || mangaConnectors?.length == null || mangaConnectors.length < 1}
|
||||||
placeholder={"Select Connector"}
|
placeholder={"Select Connector"}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@ -119,8 +133,9 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
|||||||
sx={{ '--ListItemDecorator-size': '44px', minWidth: 240 }}
|
sx={{ '--ListItemDecorator-size': '44px', minWidth: 240 }}
|
||||||
renderValue={renderValue}
|
renderValue={renderValue}
|
||||||
onChange={(_e, newValue) => {
|
onChange={(_e, newValue) => {
|
||||||
setStep(2);
|
|
||||||
setSelectedMangaConnector(mangaConnectors?.find((o) => o.name === 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>}>
|
endDecorator={<Chip size={"sm"} color={mangaConnectors?.length??0 < 1 ? "danger" : "primary"}>{mangaConnectors?.length}</Chip>}>
|
||||||
{mangaConnectors?.map((connector: IMangaConnector) => ConnectorOption(connector))}
|
{mangaConnectors?.map((connector: IMangaConnector) => ConnectorOption(connector))}
|
||||||
@ -131,9 +146,9 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
|||||||
<StepIndicator variant={step==2?"solid":"outlined"} color="primary">
|
<StepIndicator variant={step==2?"solid":"outlined"} color="primary">
|
||||||
2
|
2
|
||||||
</StepIndicator>}>
|
</StepIndicator>}>
|
||||||
<Input disabled={step < 2 || resultsLoading} placeholder={"Name or Url " + (selectedMangaConnector ? selectedMangaConnector.baseUris[0] : "")} onKeyDown={(e) => {
|
<Input disabled={resultsLoading} placeholder={"Name or Url " + (selectedMangaConnector ? selectedMangaConnector.baseUris[0] : "")} onKeyDown={(e) => {
|
||||||
setStep(2);
|
setStep(2);
|
||||||
setResults([]);
|
setResults(undefined);
|
||||||
if(e.key === "Enter") {
|
if(e.key === "Enter") {
|
||||||
StartSearch(selectedMangaConnector, e.currentTarget.value);
|
StartSearch(selectedMangaConnector, e.currentTarget.value);
|
||||||
}
|
}
|
||||||
@ -143,7 +158,7 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
|||||||
<StepIndicator variant={step==3?"solid":"outlined"} color="primary">
|
<StepIndicator variant={step==3?"solid":"outlined"} color="primary">
|
||||||
3
|
3
|
||||||
</StepIndicator>}>
|
</StepIndicator>}>
|
||||||
<Typography endDecorator={<Chip size={"sm"} color={"primary"}>{results?.length}</Chip>}>Results</Typography>
|
<Typography endDecorator={<Chip size={"sm"} color={"primary"}>{results?.length??"-"}</Chip>}>Results</Typography>
|
||||||
<Skeleton loading={resultsLoading}>
|
<Skeleton loading={resultsLoading}>
|
||||||
<Stack direction={"row"} spacing={1} flexWrap={"wrap"}>
|
<Stack direction={"row"} spacing={1} flexWrap={"wrap"}>
|
||||||
{results?.map((result) =>
|
{results?.map((result) =>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import IBackendSettings from "../../api/types/IBackendSettings.ts";
|
import IBackendSettings from "../../api/types/IBackendSettings.ts";
|
||||||
import {useCallback, useContext, useState} from "react";
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@ -10,26 +10,32 @@ import {
|
|||||||
Typography
|
Typography
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {UpdateAprilFoolsToggle} from "../../api/BackendSettings.tsx";
|
import {GetAprilFoolsToggle, UpdateAprilFoolsToggle} from "../../api/BackendSettings.tsx";
|
||||||
|
|
||||||
export default function ImageProcessing({backendSettings}: {backendSettings?: IBackendSettings}) {
|
export default function ImageProcessing({backendSettings}: {backendSettings?: IBackendSettings}) {
|
||||||
const apiUri = useContext(ApiUriContext);
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
const [value, setValue] = useState<boolean>(backendSettings?.aprilFoolsMode??false);
|
||||||
|
|
||||||
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
const valueChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
const valueChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setColor("warning");
|
setColor("warning");
|
||||||
clearTimeout(timerRef.current);
|
clearTimeout(timerRef.current);
|
||||||
console.log(e);
|
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
UpdateAprilFoolsMode(e.target.checked);
|
UpdateAprilFoolsMode(e.target.checked);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateAprilFoolsMode = useCallback((value: boolean) => {
|
useEffect(() => {
|
||||||
UpdateAprilFoolsToggle(apiUri, value)
|
setValue(backendSettings?.aprilFoolsMode??false);
|
||||||
|
}, [backendSettings]);
|
||||||
|
|
||||||
|
const UpdateAprilFoolsMode = useCallback((val: boolean) => {
|
||||||
|
UpdateAprilFoolsToggle(apiUri, val)
|
||||||
|
.then(() => GetAprilFoolsToggle(apiUri))
|
||||||
|
.then((val) => setValue(val))
|
||||||
.then(() => setColor("success"))
|
.then(() => setColor("success"))
|
||||||
.catch(() => setColor("danger"))
|
.catch(() => setColor("danger"))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
@ -43,7 +49,7 @@ export default function ImageProcessing({backendSettings}: {backendSettings?: IB
|
|||||||
<Switch disabled={backendSettings === undefined || loading}
|
<Switch disabled={backendSettings === undefined || loading}
|
||||||
onChange={valueChanged}
|
onChange={valueChanged}
|
||||||
color={color}
|
color={color}
|
||||||
defaultChecked={backendSettings?.aprilFoolsMode} />
|
checked={value} />
|
||||||
}>
|
}>
|
||||||
Toggle
|
Toggle
|
||||||
</Typography>
|
</Typography>
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,113 +1,101 @@
|
|||||||
import IBackendSettings from "../../api/types/IBackendSettings.ts";
|
import IBackendSettings from "../../api/types/IBackendSettings.ts";
|
||||||
import {useCallback, useContext, useState} from "react";
|
import {ChangeEvent, useCallback, useContext, useEffect, useRef, useState} from "react";
|
||||||
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
AccordionSummary,
|
AccordionSummary, ColorPaletteProp, Input, Stack, Switch, Typography,
|
||||||
ColorPaletteProp,
|
|
||||||
Input,
|
|
||||||
Switch,
|
|
||||||
Typography
|
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import * as React from "react";
|
import {
|
||||||
import {UpdateBWImageToggle, UpdateImageCompressionValue} from "../../api/BackendSettings.tsx";
|
GetBWImageToggle,
|
||||||
|
GetImageCompressionValue,
|
||||||
|
UpdateBWImageToggle,
|
||||||
|
UpdateImageCompressionValue
|
||||||
|
} from "../../api/BackendSettings.tsx";
|
||||||
|
|
||||||
export default function ImageProcessing({backendSettings}: {backendSettings?: IBackendSettings}) {
|
export default function ImageProcessing ({backendSettings}: { backendSettings?: IBackendSettings }) {
|
||||||
const apiUri = useContext(ApiUriContext);
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
const [loadingBw, setLoadingBw] = useState<boolean>(false);
|
useEffect(() => {
|
||||||
const [bwInputColor, setBwInputcolor] = useState<ColorPaletteProp>("neutral");
|
setBwImages(backendSettings?.bwImages??false);
|
||||||
|
setCompression(backendSettings?.compression??100);
|
||||||
|
}, [backendSettings]);
|
||||||
|
|
||||||
const timerRefBw = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
const [bwImages, setBwImages] = useState<boolean>(backendSettings?.bwImages??false);
|
||||||
const bwChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
const [bwImagesLoading, setBwImagesLoading] = useState(false);
|
||||||
setBwInputcolor("warning");
|
const [bwImagesColor, setBwImagesColor] = useState<ColorPaletteProp>("neutral");
|
||||||
clearTimeout(timerRefBw.current);
|
const bwTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
console.log(e);
|
const bwValueChanged = (e : ChangeEvent<HTMLInputElement>) => {
|
||||||
timerRefBw.current = setTimeout(() => {
|
setBwImages(e.target.checked);
|
||||||
UpdateBw(e.target.checked);
|
setBwImagesColor("warning");
|
||||||
|
clearTimeout(bwTimerRef.current);
|
||||||
|
bwTimerRef.current = setTimeout(() => {
|
||||||
|
UpdateBwImages(e.target.checked);
|
||||||
}, 1000);
|
}, 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 UpdateBw = useCallback((value: boolean) => {
|
const [compression, setCompression] = useState<number>(backendSettings?.compression??100);
|
||||||
UpdateBWImageToggle(apiUri, value)
|
const [compressionLoading, setCompressionLoading] = useState(false);
|
||||||
.then(() => setBwInputcolor("success"))
|
const [compressionColor, setCompressionColor] = useState<ColorPaletteProp>("neutral");
|
||||||
.catch(() => setBwInputcolor("danger"))
|
const compressionTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
.finally(() => setLoadingBw(false));
|
const compressionCheckedChanged = (e : ChangeEvent<HTMLInputElement>) => {
|
||||||
}, [apiUri]);
|
setCompressionColor("warning");
|
||||||
|
if(!e.target.checked)
|
||||||
const [loadingCompression, setLoadingCompression] = useState<boolean>(false);
|
setCompression(100);
|
||||||
const [compressionInputColor, setCompressionInputColor] = useState<ColorPaletteProp>("neutral");
|
else
|
||||||
const [compressionEnabled, setCompressionEnabled] = useState<boolean>((backendSettings?.compression??100) < 100);
|
setCompression(50);
|
||||||
const [compressionValue, setCompressionValue] = useState<number|undefined>(backendSettings?.compression);
|
clearTimeout(compressionTimerRef.current);
|
||||||
|
bwTimerRef.current = setTimeout(() => {
|
||||||
const timerRefCompression = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
UpdateImageCompression(e.target.checked ? 50 : 100);
|
||||||
const compressionLevelChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setCompressionInputColor("warning");
|
|
||||||
setCompressionValue(Number.parseInt(e.target.value));
|
|
||||||
clearTimeout(timerRefCompression.current);
|
|
||||||
|
|
||||||
console.log(e);
|
|
||||||
timerRefCompression.current = setTimeout(() => {
|
|
||||||
UpdateCompressionLevel(Number.parseInt(e.target.value));
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
const compressionValueChanged = (e : ChangeEvent<HTMLInputElement>) => {
|
||||||
const compressionEnableChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
|
setCompressionColor("warning");
|
||||||
setCompressionInputColor("warning");
|
setCompression(parseInt(e.target.value));
|
||||||
setCompressionEnabled(e.target.checked);
|
clearTimeout(compressionTimerRef.current);
|
||||||
clearTimeout(timerRefCompression.current);
|
bwTimerRef.current = setTimeout(() => {
|
||||||
timerRefCompression.current = setTimeout(() => {
|
UpdateImageCompression(parseInt(e.target.value));
|
||||||
UpdateCompressionLevel(e.target.checked ? compressionValue! : 100);
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
const UpdateImageCompression = useCallback((val : number) => {
|
||||||
const UpdateCompressionLevel = useCallback((value: number)=> {
|
setCompressionLoading(true);
|
||||||
setLoadingCompression(true);
|
UpdateImageCompressionValue(apiUri, val)
|
||||||
UpdateImageCompressionValue(apiUri, value)
|
.then(() => GetImageCompressionValue(apiUri))
|
||||||
.then(() => {
|
.then(setCompression)
|
||||||
setCompressionInputColor("success");
|
.then(() => setCompressionColor("success"))
|
||||||
setCompressionValue(value);
|
.catch(() => setCompressionColor("danger"))
|
||||||
})
|
.finally(() => setCompressionLoading(false));
|
||||||
.catch(() => setCompressionInputColor("danger"))
|
},[apiUri]);
|
||||||
.finally(() => setLoadingCompression(false));
|
|
||||||
}, [apiUri]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<AccordionSummary>Image Processing</AccordionSummary>
|
<AccordionSummary>Image Processing</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<Typography endDecorator={
|
<Stack>
|
||||||
<Switch disabled={backendSettings === undefined || loadingBw}
|
<Typography endDecorator={
|
||||||
onChange={bwChanged}
|
<Switch disabled={backendSettings === undefined || bwImagesLoading}
|
||||||
color={bwInputColor}
|
onChange={bwValueChanged}
|
||||||
defaultChecked={backendSettings?.bwImages} />
|
color={bwImagesColor}
|
||||||
}>
|
checked={bwImages} />
|
||||||
Black and White Images
|
}>B/W Images</Typography>
|
||||||
</Typography>
|
<Typography endDecorator={
|
||||||
<Typography endDecorator={
|
<Input type={"number"} value={compression} onChange={compressionValueChanged} startDecorator={
|
||||||
<Switch disabled={backendSettings === undefined || loadingCompression}
|
<Switch disabled={backendSettings === undefined || compressionLoading}
|
||||||
onChange={compressionEnableChanged}
|
onChange={compressionCheckedChanged}
|
||||||
color={compressionInputColor}
|
color={compressionColor}
|
||||||
defaultChecked={compressionEnabled} endDecorator={
|
checked={compression < 100} />
|
||||||
<Input
|
} />
|
||||||
defaultValue={backendSettings?.compression}
|
}>Compression</Typography>
|
||||||
disabled={!compressionEnabled || loadingCompression}
|
</Stack>
|
||||||
onChange={compressionLevelChanged}
|
|
||||||
color={compressionInputColor}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if(e.key === "Enter") {
|
|
||||||
clearTimeout(timerRefCompression.current);
|
|
||||||
// @ts-ignore
|
|
||||||
UpdateCompressionLevel(Number.parseInt(e.target.value));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
sx={{width:"70px"}}
|
|
||||||
/>
|
|
||||||
} />
|
|
||||||
}>
|
|
||||||
Image Compression
|
|
||||||
</Typography>
|
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
);
|
);
|
||||||
|
@ -7,17 +7,18 @@ import {
|
|||||||
ColorPaletteProp,
|
ColorPaletteProp,
|
||||||
Input
|
Input
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import {KeyboardEventHandler, useCallback, useContext, useState} from "react";
|
import {KeyboardEventHandler, useCallback, useContext, useEffect, useState} from "react";
|
||||||
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||||
import {ResetUserAgent, UpdateUserAgent} from "../../api/BackendSettings.tsx";
|
import {GetUserAgent, ResetUserAgent, UpdateUserAgent} from "../../api/BackendSettings.tsx";
|
||||||
|
|
||||||
export default function UserAgent({backendSettings}: {backendSettings?: IBackendSettings}) {
|
export default function UserAgent({backendSettings}: {backendSettings?: IBackendSettings}) {
|
||||||
const apiUri = useContext(ApiUriContext);
|
const apiUri = useContext(ApiUriContext);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [value, setValue] = useState<string>("");
|
const [value, setValue] = useState<string>(backendSettings?.userAgent??"");
|
||||||
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
||||||
|
|
||||||
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
|
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
|
||||||
|
if(value === undefined) return;
|
||||||
if(e.key === "Enter") {
|
if(e.key === "Enter") {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
UpdateUserAgent(apiUri, value)
|
UpdateUserAgent(apiUri, value)
|
||||||
@ -25,26 +26,32 @@ export default function UserAgent({backendSettings}: {backendSettings?: IBackend
|
|||||||
.catch(() => setColor("danger"))
|
.catch(() => setColor("danger"))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}
|
}
|
||||||
}, [apiUri])
|
}, [apiUri, value])
|
||||||
|
|
||||||
const Reset = useCallback(() => {
|
const Reset = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
ResetUserAgent(apiUri)
|
ResetUserAgent(apiUri)
|
||||||
|
.then(() => GetUserAgent(apiUri))
|
||||||
|
.then((val) => setValue(val))
|
||||||
.then(() => setColor("success"))
|
.then(() => setColor("success"))
|
||||||
.catch(() => setColor("danger"))
|
.catch(() => setColor("danger"))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [apiUri]);
|
}, [apiUri]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(backendSettings?.userAgent??"");
|
||||||
|
}, [backendSettings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<AccordionSummary>UserAgent</AccordionSummary>
|
<AccordionSummary>UserAgent</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<Input disabled={backendSettings === undefined || loading}
|
<Input disabled={backendSettings === undefined || loading}
|
||||||
placeholder={"UserAgent"}
|
placeholder={"UserAgent"}
|
||||||
defaultValue={backendSettings?.userAgent}
|
value={value}
|
||||||
onKeyDown={keyDown}
|
onKeyDown={keyDown}
|
||||||
onChange={e => setValue(e.target.value)}
|
onChange={e => setValue(e.target.value)}
|
||||||
color={color}
|
color={color}
|
||||||
endDecorator={<Button onClick={Reset} loading={loading}>Reset</Button>}
|
endDecorator={<Button onClick={Reset} loading={loading}>Reset</Button>}
|
||||||
/>
|
/>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
.header {
|
.header {
|
||||||
position: static !important;
|
position: sticky !important;
|
||||||
|
z-index: 1000;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
import Sheet from "@mui/joy/Sheet";
|
import Sheet from "@mui/joy/Sheet";
|
||||||
import {Stack, Typography} from "@mui/joy";
|
import {Box, Link, Stack, Typography} from "@mui/joy";
|
||||||
import {ReactElement} from "react";
|
import {ReactElement} from "react";
|
||||||
import './Header.css';
|
import './Header.css';
|
||||||
|
import { GitHub } from "@mui/icons-material";
|
||||||
|
|
||||||
export default function Header({children} : {children? : ReactElement<any, any> | ReactElement<any,any>[] | undefined}) : ReactElement {
|
export default function Header({children} : {children? : ReactElement<any, any> | ReactElement<any,any>[] | undefined}) : ReactElement {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet className={"header"}>
|
<Sheet className={"header"}>
|
||||||
<Stack direction={"row"}
|
<Stack direction={"row"} spacing={2} sx={{width: "100%", alignItems: "center"}}>
|
||||||
spacing={4}
|
|
||||||
sx={{
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
alignItems: "center",
|
|
||||||
}}>
|
|
||||||
<Typography level={"h2"}>Tranga</Typography>
|
|
||||||
{children}
|
{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>
|
</Stack>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,8 @@ import {
|
|||||||
AccordionGroup,
|
AccordionGroup,
|
||||||
AccordionSummary, CircularProgress, ColorPaletteProp,
|
AccordionSummary, CircularProgress, ColorPaletteProp,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle, Input
|
DialogTitle, Input,
|
||||||
|
Link, Stack
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import './Settings.css';
|
import './Settings.css';
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@ -19,6 +20,8 @@ import ImageProcessing from "./Components/Settings/ImageProcessing.tsx";
|
|||||||
import ChapterNamingScheme from "./Components/Settings/ChapterNamingScheme.tsx";
|
import ChapterNamingScheme from "./Components/Settings/ChapterNamingScheme.tsx";
|
||||||
import AprilFoolsMode from './Components/Settings/AprilFoolsMode.tsx';
|
import AprilFoolsMode from './Components/Settings/AprilFoolsMode.tsx';
|
||||||
import RequestLimits from "./Components/Settings/RequestLimits.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> =>{
|
const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
||||||
return fetch(`${apiUri}/swagger/v2/swagger.json`,
|
return fetch(`${apiUri}/swagger/v2/swagger.json`,
|
||||||
@ -84,7 +87,7 @@ export default function Settings({open, setOpen, setApiUri, setConnected}:{open:
|
|||||||
}, [checking]);
|
}, [checking]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer size={"md"} open={open} onClose={() => setOpen(false)}>
|
<Drawer size={"lg"} open={open} onClose={() => setOpen(false)}>
|
||||||
<ModalClose />
|
<ModalClose />
|
||||||
<DialogTitle>Settings</DialogTitle>
|
<DialogTitle>Settings</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@ -113,7 +116,11 @@ export default function Settings({open, setOpen, setApiUri, setConnected}:{open:
|
|||||||
<ChapterNamingScheme backendSettings={backendSettings} />
|
<ChapterNamingScheme backendSettings={backendSettings} />
|
||||||
<AprilFoolsMode backendSettings={backendSettings} />
|
<AprilFoolsMode backendSettings={backendSettings} />
|
||||||
<RequestLimits backendSettings={backendSettings} />
|
<RequestLimits backendSettings={backendSettings} />
|
||||||
|
<FlareSolverr backendSettings={backendSettings} />
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
|
<Stack spacing={2} direction="row">
|
||||||
|
<Link target={"_blank"} href={apiUri + "/swagger"}><Article />Swagger Doc</Link>
|
||||||
|
</Stack>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {deleteData, getData, patchData} from './fetchApi.tsx';
|
import {deleteData, getData, patchData, postData} from './fetchApi.tsx';
|
||||||
import IBackendSettings from "./types/IBackendSettings.ts";
|
import IBackendSettings from "./types/IBackendSettings.ts";
|
||||||
import IRequestLimits from "./types/IRequestLimits.ts";
|
import IRequestLimits from "./types/IRequestLimits.ts";
|
||||||
import {RequestLimitType} from "./types/EnumRequestLimitType.ts";
|
import {RequestLimitType} from "./types/EnumRequestLimitType.ts";
|
||||||
@ -77,4 +77,16 @@ export const GetChapterNamingScheme = async (apiUri: string) : Promise<string> =
|
|||||||
|
|
||||||
export const UpdateChapterNamingScheme = async (apiUri: string, value: string) => {
|
export const UpdateChapterNamingScheme = async (apiUri: string, value: string) => {
|
||||||
return patchData(`${apiUri}/v2/Settings/ChapterNamingScheme`, value);
|
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`);
|
||||||
}
|
}
|
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)
|
||||||
|
}
|
||||||
|
);
|
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)
|
||||||
|
}
|
||||||
|
);
|
@ -14,22 +14,22 @@ export const GetJobsWithIds = async (apiUri: string, jobIds: string[]) : Promise
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GetJobsInState = async (apiUri: string, state: JobState) : Promise<IJob[]> => {
|
export const GetJobsInState = async (apiUri: string, state: JobState) : Promise<IJob[]> => {
|
||||||
if(state == null || state == undefined)
|
if(state == null)
|
||||||
return Promise.reject("state was not provided");
|
return Promise.reject("state was not provided");
|
||||||
return await getData(`${apiUri}/v2/Job/State/${state}`) as Promise<IJob[]>;
|
return await getData(`${apiUri}/v2/Job/State/${state}`) as Promise<IJob[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GetJobsWithType = async (apiUri: string, jobType: JobType) : Promise<IJob[]> => {
|
export const GetJobsWithType = async (apiUri: string, jobType: JobType) : Promise<IJob[]> => {
|
||||||
if(jobType == null || jobType == undefined) {
|
if(jobType == null) {
|
||||||
return Promise.reject("jobType was not provided");
|
return Promise.reject("jobType was not provided");
|
||||||
}
|
}
|
||||||
return await getData(`${apiUri}/v2/Job/Type/${jobType}`) as Promise<IJob[]>;
|
return await getData(`${apiUri}/v2/Job/Type/${jobType}`) as Promise<IJob[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GetJobsOfTypeAndWithState = async (apiUri: string, jobType: JobType, state: JobState) : Promise<IJob[]> => {
|
export const GetJobsOfTypeAndWithState = async (apiUri: string, jobType: JobType, state: JobState) : Promise<IJob[]> => {
|
||||||
if(jobType == null || jobType == undefined)
|
if(jobType == null)
|
||||||
return Promise.reject("jobType was not provided");
|
return Promise.reject("jobType was not provided");
|
||||||
if(state == null || state == undefined)
|
if(state == null)
|
||||||
return Promise.reject("state was not provided");
|
return Promise.reject("state was not provided");
|
||||||
return await getData(`${apiUri}/v2/Job/TypeAndState/${jobType}/${state}`) as Promise<IJob[]>;
|
return await getData(`${apiUri}/v2/Job/TypeAndState/${jobType}/${state}`) as Promise<IJob[]>;
|
||||||
}
|
}
|
||||||
@ -88,10 +88,10 @@ export const CreateUpdateAllMetadataJob = async (apiUri: string) : Promise<strin
|
|||||||
return await putData(`${apiUri}/v2/Job/UpdateAllMetadataJob`, {}) as Promise<string[]>;
|
return await putData(`${apiUri}/v2/Job/UpdateAllMetadataJob`, {}) as Promise<string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StartJob = async (apiUri: string, jobId: string) : Promise<object | undefined> => {
|
export const StartJob = async (apiUri: string, jobId: string, startDependencies: boolean) : Promise<object | undefined> => {
|
||||||
return await postData(`${apiUri}/v2/Job/${jobId}/Start`, {});
|
return await postData(`${apiUri}/v2/Job/${jobId}/Start`, startDependencies);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StopJob = async (apiUri: string, jobId: string) : Promise<object | undefined> => {
|
export const StopJob = async (apiUri: string, jobId: string) : Promise<object | undefined> => {
|
||||||
return await postData(`${apiUri}/v2/Job/${jobId}/Stop`, {});
|
return await postData(`${apiUri}/v2/Job/${jobId}/Stop`);
|
||||||
}
|
}
|
@ -29,15 +29,13 @@ export const DeleteManga = async (apiUri: string, mangaId: string) : Promise<voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GetMangaCoverImageUrl = (apiUri: string, mangaId: string, ref: HTMLImageElement | undefined | null) : string => {
|
export const GetMangaCoverImageUrl = (apiUri: string, mangaId: string, ref: HTMLImageElement | undefined | null) : string => {
|
||||||
if(ref == null || ref == undefined)
|
if(ref == null || mangaId === DefaultManga.mangaId)
|
||||||
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=64&height=64`;
|
|
||||||
if(mangaId === DefaultManga.mangaId)
|
|
||||||
return "/blahaj.png";
|
return "/blahaj.png";
|
||||||
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`;
|
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GetChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
|
export const GetChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
if(mangaId === null || mangaId.length < 1)
|
||||||
return Promise.reject("mangaId was not provided");
|
return Promise.reject("mangaId was not provided");
|
||||||
if(mangaId === DefaultManga.mangaId)
|
if(mangaId === DefaultManga.mangaId)
|
||||||
return Promise.reject("Default Manga was requested");
|
return Promise.reject("Default Manga was requested");
|
||||||
|
@ -6,7 +6,7 @@ export function getData(uri: string) : Promise<object | undefined> {
|
|||||||
return makeRequestWrapper("GET", uri, null);
|
return makeRequestWrapper("GET", uri, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postData(uri: string, content: object | string | number | boolean) : Promise<object | undefined> {
|
export function postData(uri: string, content?: object | string | number | boolean | null) : Promise<object | undefined> {
|
||||||
return makeRequestWrapper("POST", uri, content);
|
return makeRequestWrapper("POST", uri, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,20 +22,20 @@ export function putData(uri: string, content: object | string | number | boolean
|
|||||||
return makeRequestWrapper("PUT", uri, content);
|
return makeRequestWrapper("PUT", uri, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeRequestWrapper(method: string, uri: string, content: object | string | number | null | boolean) : Promise<object | undefined>{
|
function makeRequestWrapper(method: string, uri: string, content?: object | string | number | null | boolean) : Promise<object | undefined>{
|
||||||
return makeRequest(method, uri, content)
|
return makeRequest(method, uri, content)
|
||||||
.then((result) => result as Promise<object>)
|
.then((result) => result as Promise<object>)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
return Promise.resolve(undefined);
|
return Promise.reject(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentlyRequestedEndpoints: string[] = [];
|
let currentlyRequestedEndpoints: string[] = [];
|
||||||
function makeRequest(method: string, uri: string, content: object | string | number | null | boolean) : Promise<object | void> {
|
function makeRequest(method: string, uri: string, content?: object | string | number | null | boolean) : Promise<object | void> {
|
||||||
const id = method + uri;
|
const id = method + uri;
|
||||||
if(currentlyRequestedEndpoints.find(x => x == id) != undefined)
|
if(currentlyRequestedEndpoints.find(x => x == id) != undefined)
|
||||||
return Promise.reject(`Already requested: ${method} ${uri}`);
|
return Promise.reject(`DO NOT REPORT! Already requested: ${method} ${uri}`);
|
||||||
currentlyRequestedEndpoints.push(id);
|
currentlyRequestedEndpoints.push(id);
|
||||||
return fetch(uri,
|
return fetch(uri,
|
||||||
{
|
{
|
||||||
|
@ -15,4 +15,5 @@ export default interface IBackendSettings {
|
|||||||
bwImages: boolean;
|
bwImages: boolean;
|
||||||
startNewJobTimeoutMs: number;
|
startNewJobTimeoutMs: number;
|
||||||
chapterNamingScheme: string;
|
chapterNamingScheme: string;
|
||||||
|
flareSolverrUrl: string;
|
||||||
}
|
}
|
||||||
|
@ -16,14 +16,21 @@ export enum JobType {
|
|||||||
RetrieveChaptersJob = "RetrieveChaptersJob",
|
RetrieveChaptersJob = "RetrieveChaptersJob",
|
||||||
UpdateChaptersDownloadedJob = "UpdateChaptersDownloadedJob",
|
UpdateChaptersDownloadedJob = "UpdateChaptersDownloadedJob",
|
||||||
MoveMangaLibraryJob = "MoveMangaLibraryJob",
|
MoveMangaLibraryJob = "MoveMangaLibraryJob",
|
||||||
UpdateSingleChapterDownloadedJob = "UpdateSingleChapterDownloadedJob",
|
|
||||||
UpdateCoverJob = "UpdateCoverJob"
|
UpdateCoverJob = "UpdateCoverJob"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function JobTypeToString(job: JobType | string): string {
|
||||||
|
return job.replace(/([A-Z])/g, ' $1').replace("Job", "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
export enum JobState {
|
export enum JobState {
|
||||||
FirstExecution = "FirstExecution",
|
FirstExecution = "FirstExecution",
|
||||||
Running = "Running",
|
Running = "Running",
|
||||||
Completed = "Completed",
|
Completed = "Completed",
|
||||||
CompletedWaiting = "CompletedWaiting",
|
CompletedWaiting = "CompletedWaiting",
|
||||||
Failed = "Failed"
|
Failed = "Failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JobStateToString(state: JobState | string): string {
|
||||||
|
return state.replace(/([A-Z])/g, ' $1').trim();
|
||||||
}
|
}
|
@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|