Compare commits

...

111 Commits

Author SHA1 Message Date
b8350602c5 Finish master-merge 2025-06-18 19:29:23 +02:00
b4e25a9f54 Merge branch 'master' into vite-react-ts
# Conflicts:
#	Dockerfile
#	README.md
#	Website/apiConnector.js
#	Website/index.html
#	Website/interaction.js
2025-06-18 19:28:44 +02:00
5c78896a5d Add arm64 tag to vite-react-ts build
resolves #164
2025-06-18 19:22:40 +02:00
6bee507d93 Merge pull request #168 from C9Glax/cuttingedge
Last merge before v2 transition
2025-06-18 19:21:18 +02:00
7ec740da82 Merge branch 'master' into cuttingedge
# Conflicts:
#	.github/workflows/docker-image-vite-react-ts.yml
2025-06-18 19:20:20 +02:00
e253ae3d20 Code-Typos, Formatting, redundant statements 2025-06-18 19:04:17 +02:00
84f9dc176e Image Loading (MangaCovers) with lazy load attribute, onError instead of onLoad hook, preventing requests before sizing is complete 2025-06-18 18:58:46 +02:00
84e9d70d34 Merge pull request #167 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.11.1
Bump docker/setup-buildx-action from 3.11.0 to 3.11.1
2025-06-18 18:11:33 +02:00
95f4086c24 Bump docker/setup-buildx-action from 3.11.0 to 3.11.1
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.11.0 to 3.11.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.11.0...v3.11.1)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 3.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-18 16:05:48 +00:00
4c81c571a4 Job Drawer add "Re-Run" button,
Move Chapter-Drawer to the bottom of the screen (inline with Manga-Drawer)
Format JobState and JobType (with spaces)
2025-06-18 17:51:29 +02:00
b4b4479e9b Open Links in new Tab 2025-06-18 02:04:52 +02:00
d6eeae4021 Add "/api" to default apiUriStr 2025-06-17 16:25:07 +02:00
6b05c4fb00 Fix MangaList Overflow, top-position 2025-06-17 15:40:25 +02:00
d785b6e078 Add ToolTip with Text-ReleaseStatus to Manga-Badge 2025-06-17 15:34:02 +02:00
e64f325022 Reorder Header items.
Center Logo
Move Settings/Jobs buttons to left side
2025-06-17 15:31:57 +02:00
486f6f60fb Add Links to useful ressources (GitHub, Swagger Doc) 2025-06-17 15:30:37 +02:00
01c732f6a8 Make header sticky to the top 2025-06-17 15:20:59 +02:00
e5641e0690 Fix Size of SearchCard 2025-06-17 01:47:08 +02:00
b7ee80b4a4 Re-Add Screenshots 2025-06-17 01:22:13 +02:00
9f8d874822 Fix Header Spacing and Badge Location if Backend can't be reached 2025-06-17 01:11:19 +02:00
a273af5ed9 FlareSolverr Settings 2025-06-17 00:55:47 +02:00
be704d922a Add a DO NOT REPORT disclaimer to "Already Requested" message 2025-06-16 22:48:45 +02:00
692bf3561b Fix Request being wrongly resolved (even if it failed) 2025-06-16 22:47:53 +02:00
e0ec64882b URL Search
Fix MangaConnector color
2025-06-16 22:30:33 +02:00
496e6fdde8 Fix MangaPopup missing setOpen 2025-06-16 21:23:07 +02:00
49fe38962d Merge pull request #166 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.11.0
Bump docker/setup-buildx-action from 3.10.0 to 3.11.0
2025-06-16 19:38:34 +02:00
645d3c8793 Fix Jobs Popup:
- Loading without being opened
- Cluttered view: Make it a table
2025-06-16 19:37:10 +02:00
2ef7ee6b39 Bump docker/setup-buildx-action from 3.10.0 to 3.11.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.10.0 to 3.11.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.10.0...v3.11.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 3.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-16 17:18:05 +00:00
e1b590482c Nginx route requests to api via nginx 2025-06-16 00:24:51 +02:00
94e58d2d1b Fix MangaConnectorContext (missing .Provider) 2025-06-15 23:14:11 +02:00
d5c85efcb2 Fix vite host address (localhost) 2025-06-15 23:11:09 +02:00
aed0a562f0 Remove old screenshots 2025-06-15 23:04:48 +02:00
46729bade3 Merge pull request #165 from C9Glax/dependabot/github_actions/docker/build-push-action-6.18.0
Bump docker/build-push-action from 6.17.0 to 6.18.0
2025-05-28 17:17:40 +02:00
fed7ff987e Bump docker/build-push-action from 6.17.0 to 6.18.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.17.0 to 6.18.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.17.0...v6.18.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-28 15:16:47 +00:00
ddaa066dc7 Jobs List 2025-05-20 03:14:37 +02:00
a6d2de35cf Jobs List 2025-05-20 03:12:51 +02:00
7cdc31fedb Fix ImageProcessing 2025-05-19 20:52:47 +02:00
dcf596df84 Fix AprilFoolsMode Setting 2025-05-19 20:23:31 +02:00
855fe91e69 Fix UserAgent Setting 2025-05-19 20:23:24 +02:00
c00c80503c Fix UserAgent Setting 2025-05-19 20:19:18 +02:00
4e648bd04e MangaPopup placeholder for Chapterdownload: maxdownloadedchapter 2025-05-19 20:10:58 +02:00
35e2e6ce16 MangaPopup (from List) add "Last Execution" tooltip 2025-05-19 20:06:26 +02:00
dbdb3c0168 Add "Start-Job" button to MangaPopup (from list) 2025-05-19 20:00:03 +02:00
dae653c2b4 Update Job-Dependency (backend) 2025-05-19 19:59:48 +02:00
e9a2a561cf fetchApi.tsx fix nullable content-body for postData and wrapper 2025-05-19 19:58:56 +02:00
5fe3ead54d Move the Start-Search Card to MangaList.tsx 2025-05-19 19:33:39 +02:00
6d8d0ec636 Move Popup-Tags to MangaPopup.tsx 2025-05-19 19:29:42 +02:00
724dc9b986 MangaList show Skeleton instead of empty space for loading manga
MangaCard remove Link from name, move it to Popup
2025-05-19 19:23:00 +02:00
594103689e Merge pull request #162 from C9Glax/dependabot/github_actions/docker/build-push-action-6.17.0
Bump docker/build-push-action from 6.16.0 to 6.17.0
2025-05-15 18:00:45 +02:00
6ab431fde0 Bump docker/build-push-action from 6.16.0 to 6.17.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.16.0 to 6.17.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.16.0...v6.17.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-15 15:15:48 +00:00
18257cd584 Update workflow-files 2025-05-08 00:45:32 +02:00
c6dc3cd107 Merge pull request #160 from C9Glax/dependabot/github_actions/docker/build-push-action-6.16.0
Bump docker/build-push-action from 6.15.0 to 6.16.0
2025-04-24 19:54:29 +02:00
94c47b904d Bump docker/build-push-action from 6.15.0 to 6.16.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.15.0 to 6.16.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.15.0...v6.16.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-24 15:53:47 +00:00
2a4316cf1d Update README.md 2025-03-29 21:17:46 +01:00
89586ef891 Merge pull request #158 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.6.0
Bump docker/setup-qemu-action from 3.3.0 to 3.6.0
2025-03-13 14:57:25 +01:00
33514ee375 Merge pull request #156 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.10.0
Bump docker/setup-buildx-action from 3.8.0 to 3.10.0
2025-03-13 14:57:11 +01:00
526e129fb7 Merge pull request #155 from C9Glax/dependabot/github_actions/docker/build-push-action-6.15.0
Bump docker/build-push-action from 6.14.0 to 6.15.0
2025-03-13 14:56:56 +01:00
d1dae83387 Bump docker/setup-qemu-action from 3.3.0 to 3.6.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.3.0 to 3.6.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3.3.0...v3.6.0)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-28 15:50:31 +00:00
9199a7a0e4 Bump docker/setup-buildx-action from 3.8.0 to 3.10.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.8.0 to 3.10.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.8.0...v3.10.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-26 15:51:05 +00:00
d9fa4185e2 Bump docker/build-push-action from 6.14.0 to 6.15.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.14.0 to 6.15.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.14.0...v6.15.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-26 15:51:03 +00:00
717c8cd33e Merge pull request #154 from C9Glax/dependabot/github_actions/docker/build-push-action-6.14.0
Bump docker/build-push-action from 6.13.0 to 6.14.0
2025-02-20 17:13:39 +01:00
483c2d564b Bump docker/build-push-action from 6.13.0 to 6.14.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.13.0 to 6.14.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.13.0...v6.14.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-20 15:59:01 +00:00
5807844281 Merge pull request #148 from C9Glax/dependabot/github_actions/docker/build-push-action-6.13.0
Bump docker/build-push-action from 6.12.0 to 6.13.0
2025-01-24 22:17:05 +01:00
77d9e6eee1 Bump docker/build-push-action from 6.12.0 to 6.13.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.12.0 to 6.13.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.12.0...v6.13.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-24 15:42:12 +00:00
e45e7bd5f5 Merge pull request #147 from C9Glax/dependabot/github_actions/docker/build-push-action-6.12.0
Bump docker/build-push-action from 6.11.0 to 6.12.0
2025-01-15 17:44:02 +01:00
ccab3a8027 Bump docker/build-push-action from 6.11.0 to 6.12.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.11.0 to 6.12.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.11.0...v6.12.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-15 15:39:53 +00:00
5eca8dac5d Merge pull request #145 from C9Glax/dependabot/github_actions/docker/build-push-action-6.11.0
Bump docker/build-push-action from 6.9.0 to 6.11.0
2025-01-09 01:53:33 +01:00
0b34ca7723 Merge pull request #146 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.3.0
Bump docker/setup-qemu-action from 3.2.0 to 3.3.0
2025-01-09 01:53:16 +01:00
eb943ccbed Merge pull request #143 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.8.0
Bump docker/setup-buildx-action from 3.7.1 to 3.8.0
2025-01-09 01:53:02 +01:00
514cd06d2b Bump docker/setup-qemu-action from 3.2.0 to 3.3.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.2.0 to 3.3.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3.2.0...v3.3.0)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-08 15:36:31 +00:00
2035bc8f4e Bump docker/build-push-action from 6.9.0 to 6.11.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.9.0 to 6.11.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.9.0...v6.11.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-08 15:36:29 +00:00
ece175d1db Bump docker/setup-buildx-action from 3.7.1 to 3.8.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.7.1 to 3.8.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.7.1...v3.8.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 16:17:56 +00:00
da3867962b Merge pull request #141 from C9Glax/dependabot/github_actions/docker/build-push-action-6.9.0
Bump docker/build-push-action from 6.7.0 to 6.9.0
2024-11-04 16:27:26 +01:00
ec15732e57 Merge pull request #140 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.7.1
Bump docker/setup-buildx-action from 3.6.1 to 3.7.1
2024-11-04 16:27:18 +01:00
6d10c81dff Bump docker/build-push-action from 6.7.0 to 6.9.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.7.0 to 6.9.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.7.0...v6.9.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-04 15:26:40 +00:00
a3842ba20f Bump docker/setup-buildx-action from 3.6.1 to 3.7.1
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.6.1 to 3.7.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.6.1...v3.7.1)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-04 15:26:38 +00:00
74b71d57b7 Merge pull request #117 from vonProteus/defaultApiUrl
Ability to override defaultApiUrl
2024-11-03 02:59:35 +01:00
6a5e340882 Merge branch 'cuttingedge' into defaultApiUrl 2024-11-03 02:58:39 +01:00
95801828c6 Dont build on pull 2024-10-19 19:54:52 +02:00
b70dfbf482 Update docker-image-cuttingedge.yml 2024-09-11 14:50:44 +02:00
0bc93a7e5c Merge remote-tracking branch 'github/master' into cuttingedge 2024-09-11 14:43:18 +02:00
ddd9512469 Merge pull request #119 from C9Glax/master
Merge Github workflows
2024-09-09 20:05:08 +02:00
c26e208822 Update docker-image-cuttingedge.yml 2024-09-09 20:04:16 +02:00
c656e5face Update docker-image-dev.yml 2024-09-09 20:03:57 +02:00
4addacb229 Update docker-image-master.yml 2024-09-09 20:03:08 +02:00
c1aece8200 Ability to override defaultApiUrl 2024-09-01 06:09:13 +02:00
6bd9a0b1db Decrease update frequency of website 2024-08-26 12:47:46 +02:00
c72e152a7e Merge branch 'master' of github.com:C9Glax/tranga-website into cuttingedge 2024-04-22 21:56:29 +02:00
38572a3a46 Fixes https://github.com/C9Glax/tranga/issues/171 Overwrites for settingsg that weren't changed on the frontend. 2024-04-22 21:53:12 +02:00
5b52577610 Merge pull request #82 from db-2001/remove-mdex-author
Remove Mangadex Author Rate Limit
2024-04-18 23:54:42 +02:00
3b99dbd487 Remove Mangadex Author Rate Limit 2024-04-18 17:33:08 -04:00
3eb7b63fa6 Fix https://github.com/C9Glax/tranga-website/issues/76 wrong context 2024-04-02 02:07:04 +02:00
793561dad7 Merge remote-tracking branch 'github/cuttingedge' into cuttingedge 2024-04-02 02:06:04 +02:00
acb22d770d Merge pull request #74 from C9Glax/dev
No longer need to set API-URI manually
2024-03-30 00:16:47 -04:00
e0093c65ff Merge pull request #75 from C9Glax/dev
No longer need to set API-URI manually
2024-03-30 00:16:37 -04:00
ecf9e1a243 Merge pull request #69 from YodaDaCoda/dev
feat: enable API_URL in Dockerfile
2024-03-06 21:12:21 -05:00
=
5af3005179 feat: enable API_URL in Dockerfile
fix tranga.conf not redirecting /api/ requests
2024-03-07 12:16:19 +11:00
d33ac16c7d Merge pull request #68 from db-2001/dev
another freaking semicolon mistake
2024-03-06 00:11:17 -05:00
a2e0a2375a another freaking semicolon mistake 2024-03-06 00:10:33 -05:00
9c0a7a0a50 Merge pull request #67 from db-2001/dev
more conf tweaks
2024-03-06 00:07:55 -05:00
d05b8ea76b more conf tweaks 2024-03-06 00:06:16 -05:00
e9937c02bf Merge pull request #66 from db-2001/dev
conf tweaks
2024-03-05 23:55:34 -05:00
ef5524d7b6 conf tweaks 2024-03-05 23:53:46 -05:00
8f9700ec02 Merge branch 'dev' of https://github.com/C9Glax/tranga-website into dev 2024-03-05 23:09:26 -05:00
4b86cd7104 A little fucking semi-colon 2024-03-05 23:09:17 -05:00
61e3fbd500 Merge pull request #65 from db-2001/dev
Reverse Proxy api automatically test
2024-03-05 23:05:41 -05:00
657a3d1857 Reverse Proxy api automatically test 2024-03-05 23:04:11 -05:00
b4cf22b6eb Merge pull request #63 from C9Glax/cuttingedge
Updated github workflows
2024-03-01 14:08:17 -05:00
2216e13c49 Merge pull request #62 from C9Glax/dev
Updated the docker workflow files
2024-03-01 14:07:29 -05:00
0a68da2b49 Fix docker workflow files for cuttingedge and dev branches 2024-03-01 13:48:20 -05:00
b87c159823 Merge pull request #61 from C9Glax/master
Updating Dev branch so we can actually start using it for dev things
2024-02-28 06:23:18 -05:00
56 changed files with 1024 additions and 315 deletions

View File

@ -3,6 +3,8 @@ name: Docker Image CI
on:
push:
branches: [ "cuttingedge" ]
pull_request:
branches: [ "cuttingedge" ]
workflow_dispatch:
jobs:
@ -17,12 +19,12 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage
- 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
- name: Set up Docker 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
- name: Login to Docker Hub
@ -33,12 +35,12 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push Website
uses: docker/build-push-action@v6.7.0
uses: docker/build-push-action@v6.18.0
with:
context: ./Website
context: .
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
platforms: linux/amd64,linux/arm64
pull: true
push: true
tags: |

View File

@ -17,12 +17,12 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage
- 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
- name: Set up Docker 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
- name: Login to Docker Hub
@ -33,12 +33,12 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push Website
uses: docker/build-push-action@v6.7.0
uses: docker/build-push-action@v6.18.0
with:
context: ./Website
context: ./
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
platforms: linux/amd64,linux/arm64
pull: true
push: true
tags: |

View File

@ -17,12 +17,12 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage
- 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
- name: Set up Docker 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
- name: Login to Docker Hub
@ -33,12 +33,12 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push Website
uses: docker/build-push-action@v6.7.0
uses: docker/build-push-action@v6.18.0
with:
context: .
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
platforms: linux/amd64,linux/arm64
pull: true
push: true
tags: |

View File

@ -12,6 +12,8 @@ FROM nginx:alpine3.17-slim
# Copy built files from Vite's dist folder
COPY --from=builder /app/dist /usr/share/nginx/html
#COPY --from=builder /app/tranga-website/media /usr/share/nginx/html/media
COPY ./nginx /etc/nginx
EXPOSE 80
ENV API_URL=http://tranga-api:6531
CMD ["nginx", "-g", "daemon off;"]

View File

@ -33,19 +33,9 @@
<!-- ABOUT THE PROJECT -->
## Screenshots
| Default View | Search Window | Search Results |
|-------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| ![Image](Screenshots/Screenshot%202025-03-19%20at%2002-38-47%20Tranga.png) | ![Image](Screenshots/Screenshot%202025-03-19%20at%2002-39-05%20Tranga.png)<br/>![Image](Screenshots/Screenshot%202025-03-19%20at%2002-39-45%20Tranga.png) | ![Image](Screenshots/Screenshot%202025-03-19%20at%2002-39-52%20Tranga.png)<br/>![Image](Screenshots/Screenshot%202025-03-19%20at%2002-39-58%20Tranga.png) |
| 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 | |
|----------------------------------------------------------------------------|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
| ![Image](Screenshots/Screenshot%202025-03-19%20at%2002-41-51%20Tranga.png) | ![Image](Screenshots/Screenshot%202025-03-19%20at%2002-42-02%20Tranga.png) | ![Image](Screenshots/Screenshot%202025-03-19%20at%2002-42-12%20Tranga.png) |
| | Settings Dialog | |
|-|----------------------------------------------------------------------------|-|
| | ![Image](Screenshots/Screenshot%202025-03-20%20at%2000-42-58%20Tranga.png) | |
| ![Main Page](Screenshots/Screenshot%202025-06-17%20at%2001-15-08%20Tranga.png) | ![Settings](Screenshots/Screenshot%202025-06-17%20at%2001-15-23%20Tranga.png) | ![Search Empty](Screenshots/Screenshot%202025-06-17%20at%2001-15-36%20Tranga.png) |
|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| ![Search Results](Screenshots/Screenshot%202025-06-17%20at%2001-16-32%20Tranga.png) | ![Manga Popup](Screenshots/Screenshot%202025-06-17%20at%2001-16-43%20Tranga.png) | ![Jobs Drawer](Screenshots/Screenshot%202025-06-17%20at%2001-17-37%20Tranga.png) |
## About The Project

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

26
nginx/fastcgi.conf Normal file
View 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
View 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
View 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
View 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
View 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;

View 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
View 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;

View File

@ -2305,9 +2305,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2560,9 +2560,9 @@
"license": "ISC"
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -8,8 +8,6 @@
.app-content {
position: absolute;
height: calc(100% - 60px);
top: 60px;
width: 100%;
left: 0;
}

View File

@ -2,30 +2,77 @@ import Sheet from '@mui/joy/Sheet';
import './App.css'
import Settings from "./Settings.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 {ApiUriContext} from "./api/fetchApi.tsx";
import Search from './Components/Search.tsx';
import MangaList from "./Components/MangaList.tsx";
import {CardHeight, CardWidth} from "./Components/Manga.tsx";
import {MangaConnectorContext} from "./api/Contexts/MangaConnectorContext.tsx";
import IMangaConnector from "./api/types/IMangaConnector.ts";
import {GetAllConnectors} from "./api/MangaConnector.tsx";
import JobsDrawer from "./Components/Jobs.tsx";
import {MangaContext} from "./api/Contexts/MangaContext.tsx";
import IManga from "./api/types/IManga.ts";
import {GetMangaById} from "./api/Manga.tsx";
import IChapter from "./api/types/IChapter.ts";
import {GetChapterFromId} from "./api/Chapter.tsx";
import {ChapterContext} from "./api/Contexts/ChapterContext.tsx";
export default function App () {
const [showSettings, setShowSettings] = useState<boolean>(false);
const [showSearch, setShowSearch] = useState<boolean>(false);
const [showJobs, setShowJobs] = useState<boolean>(false);
const [apiConnected, setApiConnected] = useState<boolean>(false);
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/"));
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/api";
const [apiUri, setApiUri] = useState<string>(apiUriStr);
const [mangas, setMangas] = useState<IManga[]>([]);
const [chapters, setChapters] = useState<IChapter[]>([]);
useEffect(() => {
localStorage.setItem("apiUri", apiUri);
}, [apiUri]);
const [mangaPromises, setMangaPromises] = useState(new Map<string, Promise<IManga | undefined>>());
const GetManga = (mangaId: string) : Promise<IManga | undefined> => {
const promise = mangaPromises.get(mangaId);
if(promise) return promise;
const p = new Promise<IManga | undefined>((resolve, reject) => {
let ret = mangas?.find(m => m.mangaId == mangaId);
if (ret) resolve(ret);
console.log(`Fetching manga ${mangaId}`);
GetMangaById(apiUri, mangaId).then(manga => {
if(manga && mangas?.find(m => m.mangaId == mangaId) === undefined)
setMangas([...mangas, manga]);
resolve(manga);
}).catch(reject);
});
setMangaPromises(mangaPromises.set(mangaId, p));
return p;
}
const [chapterPromises, setChapterPromises] = useState(new Map<string, Promise<IChapter | undefined>>());
const GetChapter = (chapterId: string) : Promise<IChapter | undefined> => {
const promise = chapterPromises.get(chapterId);
if(promise) return promise;
const p = new Promise<IChapter | undefined>((resolve, reject) => {
let ret = chapters?.find(c => c.chapterId == chapterId);
if (ret) resolve(ret);
console.log(`Fetching chapter ${chapterId}`);
GetChapterFromId(apiUri, chapterId).then(chapter => {
if(chapter && chapters?.find(c => c.chapterId == chapterId) === undefined)
setChapters([...chapters, chapter]);
resolve(chapter);
}).catch(reject);
});
setChapterPromises(chapterPromises.set(chapterId, p));
return p;
}
const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>([]);
useEffect(() => {
@ -35,38 +82,26 @@ export default function App () {
return (
<ApiUriContext.Provider value={apiUri}>
<MangaConnectorContext value={mangaConnectors}>
<MangaConnectorContext.Provider value={mangaConnectors}>
<MangaContext.Provider value={{mangas, GetManga}}>
<ChapterContext.Provider value={{chapters, GetChapter}}>
<Sheet className={"app"}>
<Header>
<Badge color={"danger"} invisible={apiConnected} badgeContent={"!"}>
<Button onClick={() => setShowSettings(true)}>Settings</Button>
</Badge>
<Button onClick={() => setShowJobs(true)}>Jobs</Button>
</Header>
<Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri} setConnected={setApiConnected} />
<Search open={showSearch} setOpen={setShowSearch} />
<JobsDrawer open={showJobs} connected={apiConnected} setOpen={setShowJobs} />
<Sheet className={"app-content"}>
<MangaList connected={apiConnected}>
<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 + 10 + "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 + 10 + "px"}} >
<Typography level={"h1"}>Search</Typography>
</Box>
</CardContent>
</Card>
</Badge>
</MangaList>
<MangaList connected={apiConnected} setShowSearch={setShowSearch} />
</Sheet>
</Sheet>
</MangaConnectorContext>
</ChapterContext.Provider>
</MangaContext.Provider>
</MangaConnectorContext.Provider>
</ApiUriContext.Provider>
);
}

View File

@ -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>
);
}

View 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>
);
}

View 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;
}
}

View File

@ -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>
);
}

View File

@ -1,42 +1,64 @@
import {
Badge,
Box,
Card,
CardContent, CardCover,
Link,
} from "@mui/joy";
import {Badge, Box, Card, CardContent, CardCover, Skeleton, Tooltip, Typography,} from "@mui/joy";
import IManga from "../api/types/IManga.ts";
import {CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
import {GetMangaById, GetMangaCoverImageUrl} from "../api/Manga.tsx";
import {GetMangaCoverImageUrl} from "../api/Manga.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 MangaPopup from "./MangaPopup.tsx";
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.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} /> }
</>
);
}
import {MangaContext} from "../api/Contexts/MangaContext.tsx";
export const CardWidth = 190;
export const CardHeight = 300;
const coverSx : SxProps = {
height: CardHeight + "px",
width: CardWidth + "px",
position: "relative",
}
const coverCss : CSSProperties = {
maxHeight: "calc("+CardHeight+"px + 2rem)",
maxWidth: "calc("+CardWidth+"px + 2rem)",
objectFit: "cover",
width: "calc("+CardHeight+"px + 2rem)",
height: "calc("+CardHeight+"px + 2rem)",
}
export function MangaFromId({mangaId, children} : { mangaId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
const mangaContext = useContext(MangaContext);
const [manga, setManga] = useState<IManga | undefined>(undefined);
mangaContext.GetManga(mangaId).then(setManga);
return (
manga === undefined ?
<Badge sx={{margin:"8px !important"}} badgeContent={<Skeleton><Tooltip title={"Loading"}><img width={"24pt"} height={"24pt"} src={"/blahaj.png"} /></Tooltip></Skeleton>} color={ReleaseStatusToPalette(MangaReleaseStatus.Completed)} size={"lg"}>
<Card sx={{height:"fit-content",width:"fit-content"}}>
<CardCover>
<img loading={"lazy"} style={coverCss} src={"/blahaj.png"} alt="Manga Cover"/>
</CardCover>
<CardCover sx={{
background:
'linear-gradient(to bottom, rgba(0,0,0,0.4), rgba(0,0,0,0) 200px), linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0) 300px)',
}}/>
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
<Box sx={coverSx}>
<Typography level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
<Skeleton loading={true} animation={"wave"}>
{mangaId.split("").splice(0,mangaId.length/2).join(" ")}
</Skeleton>
</Typography>
</Box>
</CardContent>
</Card>
</Badge>
:
<Manga manga={manga} children={children} />
);
}
export function Manga({manga: manga, children} : { manga: IManga, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
const CoverRef = useRef<HTMLImageElement>(null);
@ -60,34 +82,24 @@ export function Manga({manga: manga, children} : { manga: IManga, children?: Rea
getData(coverUrl).then(() => {
if(CoverRef.current) CoverRef.current.src = coverUrl;
});
}, [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)",
}
}, [manga, apiUri]);
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 (
<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) => {
const target = e.target as HTMLElement;
if(interactiveElements.find(x => x == target.localName) == undefined)
setExpanded(!expanded)}
}>
<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}
onLoad={LoadMangaCover}/>
onError={LoadMangaCover}/>
</CardCover>
<CardCover sx={{
background:
@ -95,12 +107,12 @@ export function Manga({manga: manga, children} : { manga: IManga, children?: Rea
}}/>
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
<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}
</Link>
</Typography>
</Box>
</CardContent>
<MangaPopup manga={manga} open={expanded}>{children}</MangaPopup>
<MangaPopup manga={manga} open={expanded} setOpen={setExpanded}>{children}</MangaPopup>
</Card>
</Badge>
);

View File

@ -1,14 +1,14 @@
import {Button, Stack} from "@mui/joy";
import {useCallback, useContext, useEffect, useState} from "react";
import {Badge, Box, Button, Card, CardContent, CardCover, Stack, Tooltip, Typography} from "@mui/joy";
import {Dispatch, SetStateAction, useCallback, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx";
import {DeleteJob, GetJobsWithType} from "../api/Job.tsx";
import {DeleteJob, GetJobsWithType, StartJob} from "../api/Job.tsx";
import {JobType} from "../api/types/Jobs/IJob.ts";
import IDownloadAvailableChaptersJob from "../api/types/Jobs/IDownloadAvailableChaptersJob.ts";
import {MangaFromId} from "./Manga.tsx";
import { Remove } from "@mui/icons-material";
import {CardHeight, CardWidth, MangaFromId} from "./Manga.tsx";
import {PlayArrow, Remove} from "@mui/icons-material";
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 [jobList, setJobList] = useState<IDownloadAvailableChaptersJob[]>([]);
@ -23,17 +23,16 @@ export default function MangaList({connected, children}: {connected: boolean, ch
DeleteJob(apiUri, jobId).finally(() => getJobList());
},[apiUri]);
useEffect(() => {
getJobList();
const startJob = useCallback((jobId: string) => {
StartJob(apiUri, jobId, true).finally(() => getJobList());
},[apiUri]);
useEffect(() => {
updateTimer();
getJobList();
}, [connected]);
}, [connected, apiUri]);
const timerRef = React.useRef<ReturnType<typeof setInterval>>(undefined);
const updateTimer = () => {
const updateTimer = useCallback(() => {
if(!connected){
clearTimeout(timerRef.current);
return;
@ -41,18 +40,35 @@ export default function MangaList({connected, children}: {connected: boolean, ch
if(timerRef.current === undefined) {
console.log("Added timer!");
getJobList();
timerRef.current = setInterval(() => {
getJobList();
}, 2000);
}
timerRef.current = setInterval(getJobList, 2000);
}
}
}, [getJobList, connected, timerRef]);
return(
<Stack direction="row" spacing={1} flexWrap={"wrap"}>
{children}
<Stack direction="row" spacing={1} flexWrap={"wrap"} sx={{overflowX: 'hidden', overflowY: 'auto' /* Badge overflow */}} paddingTop={"6px" /* Badge overflow */}>
<Badge invisible sx={{margin: "8px !important"}}>
<Card onClick={() => setShowSearch(true)} sx={{height:"fit-content",width:"fit-content"}}>
<CardCover sx={{margin:"var(--Card-padding)"}}>
<img src={"/blahaj.png"} style={{height: CardHeight + "px", width: CardWidth + "px"}} />
</CardCover>
<CardCover sx={{
background: 'rgba(234, 119, 246, 0.14)',
backdropFilter: 'blur(6.9px)',
webkitBackdropFilter: 'blur(6.9px)',
}}/>
<CardContent>
<Box style={{height: CardHeight + "px", width: CardWidth + "px"}} >
<Typography level={"h1"}>Search</Typography>
</Box>
</CardContent>
</Card>
</Badge>
{jobList?.map((job) => (
<MangaFromId key={job.mangaId} mangaId={job.mangaId}>
<Tooltip title={"Last run: " + new Date(job.lastExecution).toLocaleString()}>
<Button color={"success"} endDecorator={<PlayArrow />} onClick={() => startJob(job.jobId)}>Start</Button>
</Tooltip>
<Button color={"danger"} endDecorator={<Remove />} onClick={() => deleteJob(job.jobId)}>Delete</Button>
</MangaFromId>
))}

View File

@ -1,18 +1,70 @@
import IManga from "../api/types/IManga.ts";
import {Badge, Box, Chip, CircularProgress, Drawer, Input, Skeleton, Stack, Typography} from "@mui/joy";
import {ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
import {GetLatestChapterAvailable, GetMangaCoverImageUrl, SetIgnoreThreshold} from "../api/Manga.tsx";
import {Badge, Box, Chip, CircularProgress, Drawer, Input, Link, Skeleton, Stack, Typography} from "@mui/joy";
import React, {ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
import {
GetLatestChapterAvailable,
GetLatestChapterDownloaded,
GetMangaCoverImageUrl,
SetIgnoreThreshold
} from "../api/Manga.tsx";
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
import AuthorTag from "./AuthorTag.tsx";
import LinkTag from "./LinkTag.tsx";
import MarkdownPreview from "@uiw/react-markdown-preview";
import {CardHeight} from "./Manga.tsx";
import IChapter from "../api/types/IChapter.ts";
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx";
import {MangaContext} from "../api/Contexts/MangaContext.tsx";
import ModalClose from "@mui/joy/ModalClose";
export 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);
@ -21,6 +73,8 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
const LoadMangaCover = useCallback(() => {
if(CoverRef.current == null || manga == null)
return;
if (!open)
return;
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
if(CoverRef.current.src == coverUrl)
return;
@ -29,12 +83,13 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
getData(coverUrl).then(() => {
if(CoverRef.current) CoverRef.current.src = coverUrl;
});
}, [manga, apiUri])
}, [manga, apiUri, open])
useEffect(() => {
if(!open)
return;
LoadMaxChapter();
LoadDownloadedChapter();
LoadMangaCover();
}, [open]);
@ -49,8 +104,19 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
.finally(() => setMaxChapterLoading(false));
}, [manga, apiUri]);
const [mangaDownloadedChapter, setMangaDownloadedChapter] = useState<IChapter>();
const [downloadedChapterLoading, setDownloadedChapterLoading] = useState<boolean>(true);
const LoadDownloadedChapter = useCallback(() => {
if(manga == null)
return;
setDownloadedChapterLoading(true);
GetLatestChapterDownloaded(apiUri, manga.mangaId)
.then(setMangaDownloadedChapter)
.finally(() => setDownloadedChapterLoading(false));
}, [manga, apiUri]);
const [updatingThreshold, setUpdatingThreshold] = useState<boolean>(false);
const updateIgnoreThreshhold = useCallback((value: number) => {
const updateIgnoreThreshold = useCallback((value: number) => {
if(manga == null)
return;
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);
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"}>
{ /* Cover and Description */ }
<Stack direction="row" spacing={2} margin={"10px"}>
@ -70,11 +137,17 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
onLoad={LoadMangaCover}/>
</Badge>
<Box>
<Typography level={"h2"} marginTop={"20px"}>{manga?.name}</Typography>
<Stack direction={"row"} flexWrap={"wrap"} spacing={0.5} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
{manga?.authors?.map(author => <AuthorTag key={author.authorId} author={author} color={"success"} />)}
<Link target={"_blank"} href={manga?.websiteUrl} level={"h2"}>
{manga?.name}
</Link>
<Stack direction={"row"} flexWrap={"wrap"} useFlexGap={true} spacing={0.3} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
{manga?.authors?.map(author => <Chip key={author.authorId} variant={"outlined"} size={"md"} color={"success"}>{author.authorName}</Chip>)}
{manga?.mangaTags?.map(tag => <Chip key={tag.tag} variant={"soft"} size={"md"} color={"primary"}>{tag.tag}</Chip>)}
{manga?.links?.map(link => <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>
<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>
@ -84,7 +157,7 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
<Stack direction="row" spacing={2}>
<Input
type={"number"}
placeholder={"0.0"}
placeholder={downloadedChapterLoading ? "" : mangaDownloadedChapter?.chapterNumber??"0.0"}
startDecorator={
<>
{
@ -103,7 +176,7 @@ export default function MangaPopup({manga, open, children} : {manga: IManga | nu
}
sx={{width:"min-content"}}
size={"md"}
onChange={(e) => updateIgnoreThreshhold(e.currentTarget.valueAsNumber)}
onChange={(e) => updateIgnoreThreshold(e.currentTarget.valueAsNumber)}
/>
{children}
</Stack>

View File

@ -21,7 +21,7 @@ import {useCallback, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx";
import {GetAllConnectors} from "../api/MangaConnector.tsx";
import IManga from "../api/types/IManga.ts";
import {SearchNameOnConnector} from "../api/Search.tsx";
import {SearchNameOnConnector, SearchUrl} from "../api/Search.tsx";
import {Manga} from "./Manga.tsx";
import Add from "@mui/icons-material/Add";
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>>}){
const [step, setStep] = useState<number>(1);
const [step, setStep] = useState<number>(2);
const apiUri = useContext(ApiUriContext);
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 StartSearch = useCallback((mangaConnector : IMangaConnector | undefined, value: string)=>{
setStep(3);
if(mangaConnector === undefined)
if(mangaConnector === undefined && !IsValidUrl(value))
return;
setResults([]);
setResults(undefined);
setResultsLoading(true);
setStep(3);
if (IsValidUrl(value)){
SearchUrl(apiUri, value).then((r) => setResults([r])).finally(() => setResultsLoading(false));
}else if (mangaConnector != undefined){
SearchNameOnConnector(apiUri, mangaConnector.name, value).then(setResults).finally(() => setResultsLoading(false));
}
},[apiUri])
function IsValidUrl(str : string) : boolean {
const pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
'(\\#[-a-z\\d_]*)?$','i'); // fragment locator
return !!pattern.test(str);
}
const [localLibraries, setLocalLibraries] = useState<ILocalLibrary[]>();
const [localLibrariesLoading, setLocalLibrariesLoading] = useState<boolean>(true);
const [selectedLibraryId, setSelectedLibraryId] = useState<string>();
@ -101,12 +115,12 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
<ModalClose />
<Stepper orientation={"vertical"} sx={{ height: '100%', width: "calc(100% - 80px)", margin:"40px"}}>
<Step indicator={
<StepIndicator variant={step==1?"solid":"outlined"} color={mangaConnectors?.length??0 < 1 ? "danger" : "primary"}>
<StepIndicator variant={step==1?"solid":"outlined"} color={(mangaConnectors?.length??0) < 1 ? "danger" : "primary"}>
1
</StepIndicator>}>
<Skeleton loading={mangaConnectorsLoading}>
<Select
color={mangaConnectors?.length??0 < 1 ? "danger" : "neutral"}
color={(mangaConnectors?.length??0) < 1 ? "danger" : "neutral"}
disabled={mangaConnectorsLoading || resultsLoading || mangaConnectors?.length == null || mangaConnectors.length < 1}
placeholder={"Select Connector"}
slotProps={{
@ -119,8 +133,9 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
sx={{ '--ListItemDecorator-size': '44px', minWidth: 240 }}
renderValue={renderValue}
onChange={(_e, newValue) => {
setStep(2);
setSelectedMangaConnector(mangaConnectors?.find((o) => o.name === newValue));
setStep(2);
setResults(undefined);
}}
endDecorator={<Chip size={"sm"} color={mangaConnectors?.length??0 < 1 ? "danger" : "primary"}>{mangaConnectors?.length}</Chip>}>
{mangaConnectors?.map((connector: IMangaConnector) => ConnectorOption(connector))}
@ -131,9 +146,9 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
<StepIndicator variant={step==2?"solid":"outlined"} color="primary">
2
</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);
setResults([]);
setResults(undefined);
if(e.key === "Enter") {
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">
3
</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}>
<Stack direction={"row"} spacing={1} flexWrap={"wrap"}>
{results?.map((result) =>

View File

@ -1,5 +1,5 @@
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 {
Accordion,
@ -10,26 +10,32 @@ import {
Typography
} from "@mui/joy";
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}) {
const apiUri = useContext(ApiUriContext);
const [loading, setLoading] = useState<boolean>(false);
const [color, setColor] = useState<ColorPaletteProp>("neutral");
const [value, setValue] = useState<boolean>(backendSettings?.aprilFoolsMode??false);
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
const valueChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
setColor("warning");
clearTimeout(timerRef.current);
console.log(e);
timerRef.current = setTimeout(() => {
UpdateAprilFoolsMode(e.target.checked);
}, 1000);
}
const UpdateAprilFoolsMode = useCallback((value: boolean) => {
UpdateAprilFoolsToggle(apiUri, value)
useEffect(() => {
setValue(backendSettings?.aprilFoolsMode??false);
}, [backendSettings]);
const UpdateAprilFoolsMode = useCallback((val: boolean) => {
UpdateAprilFoolsToggle(apiUri, val)
.then(() => GetAprilFoolsToggle(apiUri))
.then((val) => setValue(val))
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
@ -43,7 +49,7 @@ export default function ImageProcessing({backendSettings}: {backendSettings?: IB
<Switch disabled={backendSettings === undefined || loading}
onChange={valueChanged}
color={color}
defaultChecked={backendSettings?.aprilFoolsMode} />
checked={value} />
}>
Toggle
</Typography>

View 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>
);
}

View File

@ -1,113 +1,101 @@
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 {
Accordion,
AccordionDetails,
AccordionSummary,
ColorPaletteProp,
Input,
Switch,
Typography
AccordionSummary, ColorPaletteProp, Input, Stack, Switch, Typography,
} from "@mui/joy";
import * as React from "react";
import {UpdateBWImageToggle, UpdateImageCompressionValue} from "../../api/BackendSettings.tsx";
import {
GetBWImageToggle,
GetImageCompressionValue,
UpdateBWImageToggle,
UpdateImageCompressionValue
} from "../../api/BackendSettings.tsx";
export default function ImageProcessing ({backendSettings}: { backendSettings?: IBackendSettings }) {
const apiUri = useContext(ApiUriContext);
const [loadingBw, setLoadingBw] = useState<boolean>(false);
const [bwInputColor, setBwInputcolor] = useState<ColorPaletteProp>("neutral");
useEffect(() => {
setBwImages(backendSettings?.bwImages??false);
setCompression(backendSettings?.compression??100);
}, [backendSettings]);
const timerRefBw = React.useRef<ReturnType<typeof setTimeout>>(undefined);
const bwChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
setBwInputcolor("warning");
clearTimeout(timerRefBw.current);
console.log(e);
timerRefBw.current = setTimeout(() => {
UpdateBw(e.target.checked);
const [bwImages, setBwImages] = useState<boolean>(backendSettings?.bwImages??false);
const [bwImagesLoading, setBwImagesLoading] = useState(false);
const [bwImagesColor, setBwImagesColor] = useState<ColorPaletteProp>("neutral");
const bwTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const bwValueChanged = (e : ChangeEvent<HTMLInputElement>) => {
setBwImages(e.target.checked);
setBwImagesColor("warning");
clearTimeout(bwTimerRef.current);
bwTimerRef.current = setTimeout(() => {
UpdateBwImages(e.target.checked);
}, 1000);
}
const UpdateBw = useCallback((value: boolean) => {
UpdateBWImageToggle(apiUri, value)
.then(() => setBwInputcolor("success"))
.catch(() => setBwInputcolor("danger"))
.finally(() => setLoadingBw(false));
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 [loadingCompression, setLoadingCompression] = useState<boolean>(false);
const [compressionInputColor, setCompressionInputColor] = useState<ColorPaletteProp>("neutral");
const [compressionEnabled, setCompressionEnabled] = useState<boolean>((backendSettings?.compression??100) < 100);
const [compressionValue, setCompressionValue] = useState<number|undefined>(backendSettings?.compression);
const timerRefCompression = React.useRef<ReturnType<typeof setTimeout>>(undefined);
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));
const [compression, setCompression] = useState<number>(backendSettings?.compression??100);
const [compressionLoading, setCompressionLoading] = useState(false);
const [compressionColor, setCompressionColor] = useState<ColorPaletteProp>("neutral");
const compressionTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const compressionCheckedChanged = (e : ChangeEvent<HTMLInputElement>) => {
setCompressionColor("warning");
if(!e.target.checked)
setCompression(100);
else
setCompression(50);
clearTimeout(compressionTimerRef.current);
bwTimerRef.current = setTimeout(() => {
UpdateImageCompression(e.target.checked ? 50 : 100);
}, 1000);
}
const compressionEnableChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
setCompressionInputColor("warning");
setCompressionEnabled(e.target.checked);
clearTimeout(timerRefCompression.current);
timerRefCompression.current = setTimeout(() => {
UpdateCompressionLevel(e.target.checked ? compressionValue! : 100);
const compressionValueChanged = (e : ChangeEvent<HTMLInputElement>) => {
setCompressionColor("warning");
setCompression(parseInt(e.target.value));
clearTimeout(compressionTimerRef.current);
bwTimerRef.current = setTimeout(() => {
UpdateImageCompression(parseInt(e.target.value));
}, 1000);
}
const UpdateCompressionLevel = useCallback((value: number)=> {
setLoadingCompression(true);
UpdateImageCompressionValue(apiUri, value)
.then(() => {
setCompressionInputColor("success");
setCompressionValue(value);
})
.catch(() => setCompressionInputColor("danger"))
.finally(() => setLoadingCompression(false));
const UpdateImageCompression = useCallback((val : number) => {
setCompressionLoading(true);
UpdateImageCompressionValue(apiUri, val)
.then(() => GetImageCompressionValue(apiUri))
.then(setCompression)
.then(() => setCompressionColor("success"))
.catch(() => setCompressionColor("danger"))
.finally(() => setCompressionLoading(false));
},[apiUri]);
return (
<Accordion>
<AccordionSummary>Image Processing</AccordionSummary>
<AccordionDetails>
<Stack>
<Typography endDecorator={
<Switch disabled={backendSettings === undefined || loadingBw}
onChange={bwChanged}
color={bwInputColor}
defaultChecked={backendSettings?.bwImages} />
}>
Black and White Images
</Typography>
<Switch disabled={backendSettings === undefined || bwImagesLoading}
onChange={bwValueChanged}
color={bwImagesColor}
checked={bwImages} />
}>B/W Images</Typography>
<Typography endDecorator={
<Switch disabled={backendSettings === undefined || loadingCompression}
onChange={compressionEnableChanged}
color={compressionInputColor}
defaultChecked={compressionEnabled} endDecorator={
<Input
defaultValue={backendSettings?.compression}
disabled={!compressionEnabled || loadingCompression}
onChange={compressionLevelChanged}
color={compressionInputColor}
onKeyDown={(e) => {
if(e.key === "Enter") {
clearTimeout(timerRefCompression.current);
// @ts-ignore
UpdateCompressionLevel(Number.parseInt(e.target.value));
}
}}
sx={{width:"70px"}}
/>
<Input type={"number"} value={compression} onChange={compressionValueChanged} startDecorator={
<Switch disabled={backendSettings === undefined || compressionLoading}
onChange={compressionCheckedChanged}
color={compressionColor}
checked={compression < 100} />
} />
}>
Image Compression
</Typography>
}>Compression</Typography>
</Stack>
</AccordionDetails>
</Accordion>
);

View File

@ -7,17 +7,18 @@ import {
ColorPaletteProp,
Input
} 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 {ResetUserAgent, UpdateUserAgent} from "../../api/BackendSettings.tsx";
import {GetUserAgent, ResetUserAgent, UpdateUserAgent} from "../../api/BackendSettings.tsx";
export default function UserAgent({backendSettings}: {backendSettings?: IBackendSettings}) {
const apiUri = useContext(ApiUriContext);
const [loading, setLoading] = useState<boolean>(false);
const [value, setValue] = useState<string>("");
const [value, setValue] = useState<string>(backendSettings?.userAgent??"");
const [color, setColor] = useState<ColorPaletteProp>("neutral");
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
if(value === undefined) return;
if(e.key === "Enter") {
setLoading(true);
UpdateUserAgent(apiUri, value)
@ -25,23 +26,29 @@ export default function UserAgent({backendSettings}: {backendSettings?: IBackend
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}
}, [apiUri])
}, [apiUri, value])
const Reset = useCallback(() => {
setLoading(true);
ResetUserAgent(apiUri)
.then(() => GetUserAgent(apiUri))
.then((val) => setValue(val))
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}, [apiUri]);
useEffect(() => {
setValue(backendSettings?.userAgent??"");
}, [backendSettings]);
return (
<Accordion>
<AccordionSummary>UserAgent</AccordionSummary>
<AccordionDetails>
<Input disabled={backendSettings === undefined || loading}
placeholder={"UserAgent"}
defaultValue={backendSettings?.userAgent}
value={value}
onKeyDown={keyDown}
onChange={e => setValue(e.target.value)}
color={color}

View File

@ -1,5 +1,6 @@
.header {
position: static !important;
position: sticky !important;
z-index: 1000;
top: 0;
left: 0;
width: 100%;

View File

@ -1,19 +1,27 @@
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 './Header.css';
import { GitHub } from "@mui/icons-material";
export default function Header({children} : {children? : ReactElement<any, any> | ReactElement<any,any>[] | undefined}) : ReactElement {
return (
<Sheet className={"header"}>
<Stack direction={"row"}
spacing={4}
sx={{
justifyContent: "flex-start",
alignItems: "center",
}}>
<Typography level={"h2"}>Tranga</Typography>
<Stack direction={"row"} spacing={2} sx={{width: "100%", alignItems: "center"}}>
{children}
<Box sx={{flexGrow: 1}} />
<img src={"/blahaj.png"} style={{cursor: "grab", maxHeight: "100%"}}/>
<Typography level={"h2"} sx={{
background: "linear-gradient(110deg, var(--joy-palette-primary-solidBg), var(--joy-palette-success-400))",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
fontWeight: "bold",
cursor: "default"
}}>Tranga</Typography>
<Box sx={{flexGrow: 1}} />
<Link target={"_blank"} href={"https://github.com/C9Glax/tranga"} color={"neutral"} height={"min-content"} ><GitHub /> Server</Link>
<Link target={"_blank"} href={"https://github.com/C9Glax/tranga-website"} color={"neutral"} height={"min-content"} ><GitHub /> Website</Link>
</Stack>
</Sheet>
);

View File

@ -6,7 +6,8 @@ import {
AccordionGroup,
AccordionSummary, CircularProgress, ColorPaletteProp,
DialogContent,
DialogTitle, Input
DialogTitle, Input,
Link, Stack
} from "@mui/joy";
import './Settings.css';
import * as React from "react";
@ -19,6 +20,8 @@ import ImageProcessing from "./Components/Settings/ImageProcessing.tsx";
import ChapterNamingScheme from "./Components/Settings/ChapterNamingScheme.tsx";
import AprilFoolsMode from './Components/Settings/AprilFoolsMode.tsx';
import RequestLimits from "./Components/Settings/RequestLimits.tsx";
import FlareSolverr from "./Components/Settings/FlareSolverr.tsx";
import {Article} from '@mui/icons-material';
const checkConnection = async (apiUri: string): Promise<boolean> =>{
return fetch(`${apiUri}/swagger/v2/swagger.json`,
@ -84,7 +87,7 @@ export default function Settings({open, setOpen, setApiUri, setConnected}:{open:
}, [checking]);
return (
<Drawer size={"md"} open={open} onClose={() => setOpen(false)}>
<Drawer size={"lg"} open={open} onClose={() => setOpen(false)}>
<ModalClose />
<DialogTitle>Settings</DialogTitle>
<DialogContent>
@ -113,7 +116,11 @@ export default function Settings({open, setOpen, setApiUri, setConnected}:{open:
<ChapterNamingScheme backendSettings={backendSettings} />
<AprilFoolsMode backendSettings={backendSettings} />
<RequestLimits backendSettings={backendSettings} />
<FlareSolverr backendSettings={backendSettings} />
</AccordionGroup>
<Stack spacing={2} direction="row">
<Link target={"_blank"} href={apiUri + "/swagger"}><Article />Swagger Doc</Link>
</Stack>
</DialogContent>
</Drawer>
);

View File

@ -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 IRequestLimits from "./types/IRequestLimits.ts";
import {RequestLimitType} from "./types/EnumRequestLimitType.ts";
@ -78,3 +78,15 @@ export const GetChapterNamingScheme = async (apiUri: string) : Promise<string> =
export const UpdateChapterNamingScheme = async (apiUri: string, value: string) => {
return patchData(`${apiUri}/v2/Settings/ChapterNamingScheme`, value);
}
export const SetFlareSolverrUrl = async (apiUri: string, value: string) => {
return postData(`${apiUri}/v2/Settings/FlareSolverr/Url`, value);
}
export const ResetFlareSolverrUrl = async (apiUri: string) => {
return deleteData(`${apiUri}/v2/Settings/FlareSolverr/Url`);
}
export const TestFlareSolverrUrl = async (apiUri: string) => {
return postData(`${apiUri}/v2/Settings/FlareSolverr/Test`);
}

View 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)
}
);

View 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)
}
);

View File

@ -14,22 +14,22 @@ export const GetJobsWithIds = async (apiUri: string, jobIds: string[]) : Promise
}
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 await getData(`${apiUri}/v2/Job/State/${state}`) as 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 await getData(`${apiUri}/v2/Job/Type/${jobType}`) as Promise<IJob[]>;
}
export const GetJobsOfTypeAndWithState = async (apiUri: string, jobType: JobType, state: JobState) : Promise<IJob[]> => {
if(jobType == null || jobType == undefined)
if(jobType == null)
return Promise.reject("jobType was not provided");
if(state == null || state == undefined)
if(state == null)
return Promise.reject("state was not provided");
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[]>;
}
export const StartJob = async (apiUri: string, jobId: string) : Promise<object | undefined> => {
return await postData(`${apiUri}/v2/Job/${jobId}/Start`, {});
export const StartJob = async (apiUri: string, jobId: string, startDependencies: boolean) : Promise<object | undefined> => {
return await postData(`${apiUri}/v2/Job/${jobId}/Start`, startDependencies);
}
export const StopJob = async (apiUri: string, jobId: string) : Promise<object | undefined> => {
return await postData(`${apiUri}/v2/Job/${jobId}/Stop`, {});
return await postData(`${apiUri}/v2/Job/${jobId}/Stop`);
}

View File

@ -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 => {
if(ref == null || ref == undefined)
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=64&height=64`;
if(mangaId === DefaultManga.mangaId)
if(ref == null || mangaId === DefaultManga.mangaId)
return "/blahaj.png";
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`;
}
export const GetChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
if(mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");

View File

@ -6,7 +6,7 @@ export function getData(uri: string) : Promise<object | undefined> {
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);
}
@ -22,20 +22,20 @@ export function putData(uri: string, content: object | string | number | boolean
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)
.then((result) => result as Promise<object>)
.catch((e) => {
console.warn(e);
return Promise.resolve(undefined);
return Promise.reject(e);
});
}
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;
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);
return fetch(uri,
{

View File

@ -15,4 +15,5 @@ export default interface IBackendSettings {
bwImages: boolean;
startNewJobTimeoutMs: number;
chapterNamingScheme: string;
flareSolverrUrl: string;
}

View File

@ -16,10 +16,13 @@ export enum JobType {
RetrieveChaptersJob = "RetrieveChaptersJob",
UpdateChaptersDownloadedJob = "UpdateChaptersDownloadedJob",
MoveMangaLibraryJob = "MoveMangaLibraryJob",
UpdateSingleChapterDownloadedJob = "UpdateSingleChapterDownloadedJob",
UpdateCoverJob = "UpdateCoverJob"
}
export function JobTypeToString(job: JobType | string): string {
return job.replace(/([A-Z])/g, ' $1').replace("Job", "").trim();
}
export enum JobState {
FirstExecution = "FirstExecution",
Running = "Running",
@ -27,3 +30,7 @@ export enum JobState {
CompletedWaiting = "CompletedWaiting",
Failed = "Failed"
}
export function JobStateToString(state: JobState | string): string {
return state.replace(/([A-Z])/g, ' $1').trim();
}

View File

@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: '127.0.0.1',
}
})