Compare commits

...

288 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
a59920f09d Fix results null 2025-05-18 22:42:52 +02:00
5c5f810127 MangaPopup Badge Icon 2025-05-18 22:38:40 +02:00
181d591344 useState for MangaConnector in Manga with loading from id 2025-05-18 22:26:06 +02:00
0193077377 MangaConnectorContext 2025-05-18 22:01:21 +02:00
9fa57eacdb Fix BadgeIcon-Size 2025-05-18 21:57:27 +02:00
c4cc21f866 Dependency Update 2025-05-18 20:34:47 +02:00
f391ace9b2 fetchApi dont throw rejected promises 2025-05-18 19:52:38 +02:00
a0d15e08a7 Manga Card with Icon as Badge 2025-05-18 17:28:01 +02:00
11549a07c3 MangaPopup Drawer 2025-05-17 19:25:31 +02:00
79c13ceb1d Fix Manga-Item to new interfaces,
fix Manga-cover
2025-05-17 18:29:21 +02:00
0907031583 API Dependency-Update 2025-05-17 17:59:30 +02:00
00932bf6bd Remove Lunasea 2025-05-16 19:24: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
bbad467fe6 Cutoff MangaName in CoverCard 2025-05-08 05:26:51 +02:00
03c78f7b6a Cardsizing in const 2025-05-08 05:25:36 +02:00
3ad7fbfad2 Make ApiUri persistent 2025-05-08 05:23:31 +02:00
6173d16edc MangaCard fix Margins 2025-05-08 05:12:36 +02:00
8a07818b89 MangaCard fix Margins 2025-05-08 05:02:46 +02:00
6aa53f7ca7 Fix some sizing on MangaCard 2025-05-08 04:55:06 +02:00
415e6c606f Make Manga-Cards smaller,
Manga-Cover covers whole Card
2025-05-08 04:40:09 +02:00
178e8b72f3 Remove debug 2025-05-08 02:29:08 +02:00
65457c289f Favicon and Page-Title 2025-05-08 02:23:48 +02:00
6109bb678a Reload Cover/Latest available on load 2025-05-08 02:20:39 +02:00
cc14c5adad Debug 2025-05-08 02:20:24 +02:00
8c0f61fc6a update dockerfile 2025-05-08 02:20:24 +02:00
82389c4847 installed @uiw/react-markdown-preview 2025-05-08 01:22:52 +02:00
a91452ee8c Update to mui 2025-05-08 01:12:48 +02:00
698e16cc2b Update to mui 2025-05-08 00:57:15 +02:00
4e20b95d77 Merge pull request #161 from C9Glax/ant-design
React with mui/joy
2025-05-08 00:46:21 +02:00
18257cd584 Update workflow-files 2025-05-08 00:45:32 +02:00
5626a9d0fd Remove Lunasea https://github.com/C9Glax/tranga/issues/389 2025-05-08 00:32:09 +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
10f48af9fa Styling 2025-04-02 02:17:10 +02:00
5e95c34306 Reset Step to 2 on search drawer close if greater 2 2025-04-02 01:22:50 +02:00
66dcb2f1e6 RequestLimits Reset All position 2025-04-02 01:20:00 +02:00
19e033995d Search Styling 2025-04-02 01:18:49 +02:00
99265bacb2 UserAgent Reset 2025-04-02 01:06:55 +02:00
5385dfd918 Request Limits Reset 2025-04-02 01:04:37 +02:00
d6e4d1d27f Request Limits 2025-04-02 00:52:35 +02:00
288cd77049 April Fools Mode 2025-04-02 00:19:26 +02:00
e605578a34 Chapter Naming Scheme Settings 2025-04-02 00:16:13 +02:00
6340c5ad03 Image Processing Settings 2025-04-02 00:06:57 +02:00
6d402357e7 UserAgent Setting 2025-04-01 19:17:46 +02:00
9e0eb0262e MangaList 2025-04-01 19:05:57 +02:00
63b220bd79 Add Badge to Search Results to show number of results 2025-04-01 18:29:27 +02:00
d480f62e51 Warning icon if API not connected 2025-04-01 18:16:57 +02:00
44755675e5 LocalLibrary Select in Manga Search 2025-04-01 18:10:30 +02:00
6b10aa8926 @mui/joy 2025-04-01 04:13:50 +02:00
48b669dc07 asset include 2025-03-30 17:34:58 +02:00
fbb25807e6 asset include 2025-03-30 17:32:50 +02:00
44f43b7152 asset include 2025-03-30 17:23:23 +02:00
68653778c8 wrong reference 2025-03-30 17:21:21 +02:00
fd4c01203f Build app before deploy
https://github.com/C9Glax/tranga/pull/355#issuecomment-2764253691
2025-03-30 17:10:25 +02:00
2a4316cf1d Update README.md 2025-03-29 21:17:46 +01:00
d358611133 https://github.com/C9Glax/tranga/issues/246 2025-03-29 21:00:33 +01:00
b0cbb416d6 Custom Chapter Naming Scheme
https://github.com/C9Glax/tranga/issues/92
2025-03-29 20:34:09 +01:00
e3ad70c0a9 Settings to enable and disable MangaConnectors 2025-03-27 19:50:41 +01:00
f38290b2ed #50 2025-03-27 19:40:38 +01:00
211db3d4d5 Only make 1 request to an endpoint concurrently 2025-03-20 00:58:12 +01:00
a3046680ac Update Job-Footer only every 5 seconds 2025-03-20 00:57:53 +01:00
1ed376ba47 Do not update Queue-preview when not shown 2025-03-20 00:57:23 +01:00
63b10600da README 2025-03-20 00:43:58 +01:00
5333c025f7 Fix loaders 2025-03-20 00:41:35 +01:00
187dd22027 Backend Settings 2025-03-20 00:07:20 +01:00
2092db2ba3 Local Libraries in Settings 2025-03-19 22:44:11 +01:00
ecd76712d8 Add all types of Notification Connectors to settings 2025-03-19 22:01:07 +01:00
60f957ede2 Fix readme 2025-03-19 02:54:07 +01:00
fcd232bec9 Update Screenshots and readme 2025-03-19 02:51:32 +01:00
357fca3bd5 Fix DELETE functions
Fix ADDing new Manga
2025-03-19 02:44:55 +01:00
1d8dd7381d Add Loader-Spinner
Style Settings, re-add api-url-field
2025-03-19 02:37:36 +01:00
0b0abb3801 Add Settings Popup
Add NotificationConnector Setting (need to add styling)
2025-03-17 21:07:27 +01:00
62665f5660 Naming 2025-03-17 20:20:45 +01:00
007b49c624 Single Style for Manga 2025-03-16 22:25:43 +01:00
d9bbbed1c0 Multiple Base-Paths support 2025-03-16 16:48:38 +01:00
42a1e1a2ce Add Readme Screenshots 2025-03-14 01:00:10 +01:00
aad18c0195 Update Readme 2025-03-14 00:50:38 +01:00
1eef710efc Adding a removing jobs.
Having a list of running and waiting jobs
2025-03-14 00:41:07 +01:00
84520d8e18 Adjust all endpoints and methods to tranga/postgres-Server-V2.
Search working.
2025-03-13 20:08:37 +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
0402f9e6d0 Change Cover to request 1.5 times the size of actual view 2024-10-31 21:59:41 +01:00
1f8cacb668 Request scaled images 2024-10-31 21:40:01 +01:00
e58d3f8741 Tooltips for some settings 2024-10-27 04:15:25 +01:00
7eca06332a Change compression option to integer range 2024-10-27 03:54:24 +01:00
363d4d7518 Add Compression options for Images 2024-10-27 03:44:06 +01:00
4660f9a402 IntervalStringFromDate: Fix date being a string 2024-10-27 03:43:47 +01:00
07dfee15eb Footer: Change order of statuses 2024-10-27 03:43:24 +01:00
6b3e2899f0 Include UpdateMetaData in queue popup 2024-10-27 03:01:05 +01:00
251ec0a978 Fix Aprilfoolsmode toggle 2024-10-27 02:32:05 +01:00
a486b5bdfc Added April Fools Mode toggle 2024-10-23 03:01:23 +02:00
f426a77f25 Simplified frontend setting creation.
Added ability to reset UserAgent
2024-10-23 02:50:26 +02:00
24417ae180 Added settings Changes. 2024-10-23 02:28:36 +02:00
3cd64b9bfb Fix default export of Manga and Job classes 2024-10-22 18:51:34 +02:00
c525957b2e Fix width of settings section 2024-10-22 18:45:08 +02:00
46ea39245e Settings styling 2024-10-22 18:32:20 +02:00
d621422ae3 Clear inputs when settings changed 2024-10-22 18:12:24 +02:00
f4011a7cbc Fix Settings not updating ApiUri 2024-10-22 18:06:12 +02:00
a383ded819 Update JobsList when adding/removing Jobs 2024-10-20 21:29:07 +02:00
7364a20d5d Add Progressbar to jobs
Add Cancel-Buttons to running jobs
Auto-update Queue
2024-10-20 21:20:18 +02:00
f3a091f09d Make settings-gear icon white 2024-10-20 20:27:47 +02:00
fc3fa627c1 Add "alt" tags to images
Remove unused code
2024-10-20 20:24:52 +02:00
5e4e64f019 Fix covers not loading 2024-10-20 20:24:00 +02:00
0a0e901ef7 Favicon :3 2024-10-20 20:22:06 +02:00
746ad1c16c Remove all logging from console 2024-10-20 20:17:31 +02:00
67bed57ab6 Remove all logging from console 2024-10-20 20:16:32 +02:00
d97eff9796 Make apiUri changeable from frontend 2024-10-20 20:12:27 +02:00
d2533ee98f Settings Template and Popup in Header bar 2024-10-20 17:54:38 +02:00
fcc1ff392c Created standardized Popup-Window
Moved Update-functions for Queue-Status and Monitoring-list to their respective modules
2024-10-20 17:27:02 +02:00
1304bc750a Check backend connection at intervals, not just on startup 2024-10-20 16:06:21 +02:00
4aff0ed5e0 Fix height for queuepopup texts 2024-10-20 02:41:35 +02:00
dbd6684faf Fix Max-width for covers in QueuePopup 2024-10-20 02:40:31 +02:00
73a1725eb9 Fix QueuePopUp not showing any jobs 2024-10-20 02:40:17 +02:00
efdcba57ae WWrap tags on search results 2024-10-20 02:34:30 +02:00
5cb638d9c1 Move MonitorList Entry Buttons to correct stylefile,
replace buttons with clickable divs
2024-10-20 02:33:04 +02:00
eef819d140 Fixup Queue Popup 2024-10-20 02:28:26 +02:00
d04e0f6374 Add Basic QueuePopup that opens when clicking in the footer. 2024-10-20 02:09:30 +02:00
ca3aa2e8e8 Add IChapter 2024-10-20 02:09:12 +02:00
442b2ce0cc IMange export the ReleaseStatusFromNumber function 2024-10-20 02:09:03 +02:00
004f96194f Fix GetMangaByIds (change request string to "mangaIds") 2024-10-20 02:08:44 +02:00
c1d333e002 Add GetStandbyJobs call and display to footer.
Fix IJob to represent possible return values.
2024-10-20 02:08:22 +02:00
96b5163486 Add delete and startnow buttons to Monitoring Entries
nowrap tags in searchresults
2024-10-20 00:30:28 +02:00
035d384411 Fix search not working on enter 2024-10-20 00:05:13 +02:00
59dff529ef Add Markdown support to Manga-Description 2024-10-19 23:47:08 +02:00
5aac05ae08 Remove unnecessary imports 2024-10-19 21:10:59 +02:00
db61837457 Increase Interval between updates. 2024-10-19 21:08:59 +02:00
756998847c Change console levels 2024-10-19 21:08:11 +02:00
ad6502d824 Style "Monitor"-Button in Searchresults 2024-10-19 21:06:32 +02:00
f3cb3f209c Added ability to submit inputbox on search 2024-10-19 21:02:00 +02:00
da002df3f2 Add "Close Search" Button 2024-10-19 20:57:03 +02:00
68887d65a7 Make searchbox size adjust to length 2024-10-19 20:50:14 +02:00
6b08123ee5 Change bordercolor of search
Add margin to Searchresults
2024-10-19 20:41:02 +02:00
adf876c37f Add link to Website to MangaSearchResult 2024-10-19 20:37:33 +02:00
d67b1754f9 Auto-update footer counts 2024-10-19 20:15:53 +02:00
09be1c64a3 Style footer Job-counts. 2024-10-19 20:06:40 +02:00
30f3903662 Add .vite to .gitignore 2024-10-19 19:56:02 +02:00
95801828c6 Dont build on pull 2024-10-19 19:54:52 +02:00
3f26d3bbd6 Add auto-update when adding/removing Manga
Style SearchResults
2024-10-19 19:52:28 +02:00
daa05a0b4d Added button to open Search-dialog 2024-10-19 16:28:49 +02:00
2f9eb61377 Add monitorMangaList to App 2024-10-18 19:46:17 +02:00
65814dd942 Show Job Status in Footer 2024-10-18 19:45:23 +02:00
dbad993c7a Add Job-Interface (and ProgressToken) 2024-10-18 19:45:04 +02:00
ac8ca1f886 Split Styles for modules into separate files 2024-10-18 19:44:15 +02:00
f27f11e7c2 Style Manga-Display
Fix Manga.GetMangaCoverUrl
2024-10-18 19:43:13 +02:00
9d8dadc634 Add postData and deleteData functions.
Return promise.reject() on failure
2024-10-18 19:42:17 +02:00
ca091bac92 Add connection to backend test 2024-10-18 02:20:22 +02:00
cf09bc50fb Add Header, Footer, Basic Search 2024-10-18 02:10:58 +02:00
d5115809ca Clean/remove everything 2024-10-17 19:46:47 +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
d4efb158ef Merge pull request #122 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.6.1
Bump docker/setup-buildx-action from 3.3.0 to 3.6.1
2024-09-11 14:42:29 +02:00
fba5764328 Merge pull request #123 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.2.0
Bump docker/setup-qemu-action from 2.2.0 to 3.2.0
2024-09-11 14:42:17 +02:00
101a269042 Merge pull request #124 from C9Glax/dependabot/github_actions/docker/build-push-action-6.7.0
Bump docker/build-push-action from 4.1.1 to 6.7.0
2024-09-11 14:38:50 +02:00
3c45813c61 Bump docker/build-push-action from 4.1.1 to 6.7.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4.1.1 to 6.7.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4.1.1...v6.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-11 12:18:30 +00:00
cb38939888 Bump docker/setup-qemu-action from 2.2.0 to 3.2.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2.2.0 to 3.2.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2.2.0...v3.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-11 12:18:28 +00:00
eb48cf3733 Bump docker/setup-buildx-action from 3.3.0 to 3.6.1
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.3.0 to 3.6.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.3.0...v3.6.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-09-11 12:18:26 +00: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
8eda0dcb1d Merge pull request #78 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.3.0
Bump docker/setup-buildx-action from 3.1.0 to 3.3.0
2024-04-22 21:56:08 +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
b36cbe1d90 Bump docker/setup-buildx-action from 3.1.0 to 3.3.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.1.0 to 3.3.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.1.0...v3.3.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-04-08 15:49:51 +00: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
6fac397e36 Merge pull request #41 from db-2001/settings-rework
Settings rework with Filter changes
2024-02-28 03:22:44 +01:00
31a03a54bb Merge pull request #57 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.1.0
Bump docker/setup-buildx-action from 2.10.0 to 3.1.0
2024-02-28 02:35:49 +01:00
83ed99e5db Bump docker/setup-buildx-action from 2.10.0 to 3.1.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.10.0 to 3.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.10.0...v3.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-27 15:31:55 +00:00
5cda6ece05 Potential fix/support for HTTPS
Removed the hard-coded http in the API url and instead calling  window.location.protocol
2024-02-24 10:27:14 -05:00
b99ade74c7 Commented out console logging of logs 2024-02-11 18:41:21 -05:00
4f6b07280a Finalized Downlolad Logs button
B)
2024-02-11 18:39:27 -05:00
01067006f8 Commenting out console logging of HTTP request response 2024-02-11 17:01:37 -05:00
4c418add81 Updates to match API 2024-02-11 16:59:05 -05:00
1ee17360e5 Small visual tweak to make the user agent the textbox value rather than placeholder
Allows for the user to make small changes to API URI and user agent if needed, previous method would have required re-typing in the entire string with is a PITA.
2024-02-10 13:54:24 -05:00
9accca0ad2 Applied new popup style to Jobs view and fixed manga library populating twice sometimes 2024-02-10 13:49:51 -05:00
9309a4e28f Potential fix for preventing the filter buttons from loading multiple times 2024-02-10 11:56:19 -05:00
c75f65f3d5 Remove redundant refresh library 2024-02-09 11:20:09 -05:00
c7e49fb6bc Added in remaining Tranga settings functionality
Changed CSS style to be stored in local storage since we might want it to be browser specific (mobile vs desktop vs tablet)
2024-02-09 11:17:33 -05:00
2c5a032890 Added rate limits to UpdateSettings 2024-02-08 23:56:31 -05:00
8bd6ad4c2b Added manga sources to settings
Added the manga sources to the settings page and added a field to set the rate limit, POST api for setting the rate limit awaiting GET api calls to set initial value.
2024-02-08 23:31:31 -05:00
148af6abaa Filter functionality
Removed textbox in top bar and added filter icon that opens up a box where you can search or select a connector/status and combination to filter by. Appearance and behavior needs to be refined.
2024-02-06 22:23:19 -05:00
1ba49ddf08 Added PostData to reset functions 2024-02-04 16:02:09 -05:00
f50990dafb Reimplemented Library and Notification connectors
Added Test and Rest API calls, API side will need to be updated.

Separated out CSS files for future expansion.
2024-02-04 15:47:13 -05:00
76f6216501 Initial commit
Resized settings pop-up and fixed scrollable behavior, broke everything inside pop-up (on purpose)
2024-02-03 00:41:12 -05:00
18d3a09d93 https://github.com/C9Glax/tranga/issues/109 2024-01-31 19:12:05 +01:00
0bc3147bb3 #36 Remove docker-compose 2023-12-31 17:32:23 +01:00
43cbb80eec Fix overflow of tooltip release-status. 2023-11-12 13:48:28 +01:00
95876f1a53 New scrollbar, fix some overflow by scrollbar 2023-11-12 13:28:41 +01:00
42caebf458 Merge pull request #33 from db-2001/update-library-metadat
Added button to settings pop-up to refresh library metadata through POST request
2023-11-03 09:56:36 +01:00
ddee459aa2 Merge pull request #32 from db-2001/persistent-release-status
Move release-status to top right and keep visible
2023-11-03 09:50:24 +01:00
6952bcaa1d Merge pull request #31 from db-2001/hover-css-onclick
Changed CSS feature to update whenever the checkbox is clicked instead of when the update button is clicked
2023-11-03 09:48:46 +01:00
4cd06bd0e0 Added button to settings pop-up to refresh library metadata through POST request 2023-11-02 14:28:28 -04:00
d138094a66 Move release-status to top right and keep visible 2023-11-02 14:07:17 -04:00
d1dfeadb80 Feature 2023-11-02 13:31:48 -04:00
117 changed files with 9418 additions and 2591 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@v2.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@v2.10.0
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@v4.1.1
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

@ -3,8 +3,6 @@ name: Docker Image CI
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
workflow_dispatch:
jobs:
@ -19,12 +17,12 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.2.0
uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.10.0
uses: docker/setup-buildx-action@v3.11.1
# https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub
@ -35,12 +33,12 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push Website
uses: docker/build-push-action@v4.1.1
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

@ -2,7 +2,7 @@ name: Docker Image CI
on:
push:
branches: [ "cuttingedge" ]
branches: [ "vite-react-ts" ]
workflow_dispatch:
jobs:
@ -17,12 +17,12 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.2.0
uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.10.0
uses: docker/setup-buildx-action@v3.11.1
# https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub
@ -33,13 +33,13 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push Website
uses: docker/build-push-action@v4.1.1
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: |
glax/tranga-website:dev
glax/tranga-website:Server-V2

7
.gitignore vendored
View File

@ -19,3 +19,10 @@ riderModule.iml
/.idea
cover.jpg
cover.png
.vs/slnx.sqlite
.vs/tranga-website/config/applicationhost.config
.vs/tranga-website/FileContentIndex/91a465d3-1190-42e0-95eb-fa3694744e58.vsidx
.vs/tranga-website/v17/.wsuo
.vs/VSWorkspaceState.json
/node_modules/
/.vite/

View File

@ -1,4 +1,19 @@
FROM nginx:alpine3.17-slim
COPY . /usr/share/nginx/html
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY ./tranga-website /app
RUN npm install
RUN npm run build
# Serve stage
FROM nginx:alpine3.17-slim
# Copy built files from Vite's dist folder
COPY --from=builder /app/dist /usr/share/nginx/html
#COPY --from=builder /app/tranga-website/media /usr/share/nginx/html/media
COPY ./nginx /etc/nginx
EXPOSE 80
ENV API_URL=http://tranga-api:6531
CMD ["nginx", "-g", "daemon off;"]

105
README.md
View File

@ -1,16 +1,6 @@
<!-- PROJECT SHIELDS -->
<!--
*** I'm using markdown "reference style" links for readability.
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
*** See the bottom of this document for the declaration of the reference variables
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
-->
<!-- PROJECT LOGO -->
<br />
<div align="center">
<span id="readme-top"></span>
<h3 align="center">Tranga-Website</h3>
<p align="center">
@ -19,45 +9,48 @@
<p align="center">
This is the Website for <a href="https://github.com/C9Glax/tranga">Tranga</a> (API)
</p>
![GitHub License](https://img.shields.io/github/license/C9glax/tranga-website)
<table>
<tr>
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga-website/master?label=master"></th>
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga-website%2Factions%2Fworkflows%2Fdocker-image-master.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
</tr>
<tr>
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga-website/cuttingedge?label=cuttingedge"></th>
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga-website%2Factions%2Fworkflows%2Fdocker-image-cuttingedge.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
</tr>
<tr>
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga-website/vite-react-ts?label=vite-react-ts"></th>
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga-website%2Factions%2Fworkflows%2Fdocker-image-vite-react-ts.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
</tr>
</table>
</div>
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#about-the-project">About The Project</a>
<ul>
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#getting-started">Getting Started</a>
</li>
<li><a href="#roadmap">Roadmap</a></li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#license">License</a></li>
<li><a href="#acknowledgments">Acknowledgments</a></li>
</ol>
</details>
<!-- ABOUT THE PROJECT -->
## Screenshots
| ![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
Tranga-Website is the Web-frontend to [Tranga](https://github.com/C9Glax/tranga) (the API). It displays information aquired from Tranga and can create Jobs (Manga-Downloads).
Tranga-Website is the Web-frontend to [Tranga](https://github.com/C9Glax/tranga) (the API).
### What this does do (and nothing else)
This repo makes HTTP-requests to the [Tranga-API](https://github.com/C9Glax/tranga) to display it's present configuration.
This project makes HTTP-requests to the [Tranga-API](https://github.com/C9Glax/tranga) to display and modify the present configuration.
### Built With
## Built With
- nginx
- HTML, CSS, and barebones Javascript
- vite
- react
- typescript
- 💙 Blåhaj 🦈
<p align="right">(<a href="#readme-top">back to top</a>)</p>
@ -65,49 +58,17 @@ This repo makes HTTP-requests to the [Tranga-API](https://github.com/C9Glax/tran
<!-- GETTING STARTED -->
## Getting Started
There is a single release:
### Docker
Download [docker-compose.yaml](https://github.com/C9Glax/tranga-website/blob/cuttingedge/docker-compose.yaml) and configure to your needs.
The `docker-compose` also includes [Tranga](https://github.com/C9Glax/tranga) as backend. For its configuration refer to the repo README.
<!-- ROADMAP -->
## Roadmap
- [ ]
See the [open issues](https://github.com/C9Glax/tranga-website/issues) for a full list of proposed features (and known issues).
<p align="right">(<a href="#readme-top">back to top</a>)</p>
Go to [Tranga](https://github.com/C9Glax/tranga?tab=readme-ov-file#getting-started) and read the README there.
<!-- CONTRIBUTING -->
## Contributing
The following is copy & pasted:
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
Don't forget to give the project a star! Thanks again!
1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
<p align="right">(<a href="#readme-top">back to top</a>)</p>
Go to [Tranga](https://github.com/C9Glax/tranga?tab=readme-ov-file#contributing) and read the README there.
<!-- LICENSE -->
## License
Distributed under the GNU GPLv3 License. See `LICENSE.txt` for more information.
See `LICENSE.txt` for more information.
<p align="right">(<a href="#readme-top">back to top</a>)</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

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

View File

@ -1,210 +0,0 @@
let apiUri = `http://${window.location.host.split(':')[0]}:6531`
if(getCookie("apiUri") != ""){
apiUri = getCookie("apiUri");
}
setCookie("apiUri", apiUri);
function setCookie(cname, cvalue) {
const d = new Date();
d.setTime(d.getTime() + (365*24*60*60*1000));
let expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/;samesite=strict";
}
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
async function GetData(uri){
let request = await fetch(uri, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
let json = await request.json();
return json;
}
function PostData(uri){
fetch(uri, {
method: 'POST'
});
}
function DeleteData(uri){
fetch(uri, {
method: 'DELETE'
});
}
async function Ping(){
let ret = await GetData(`${apiUri}/Ping`);
return ret;
}
async function GetAvailableControllers(){
var uri = apiUri + "/Connectors";
let json = await GetData(uri);
return json;
}
async function GetPublicationFromConnector(connector, title){
var uri;
if(title.includes("http")){
uri = `${apiUri}/Manga/FromConnector?connector=${connector}&url=${title}`;
}else{
uri = `${apiUri}/Manga/FromConnector?connector=${connector}&title=${title}`;
}
let json = await GetData(uri);
return json;
}
async function GetChapters(connector, internalId, language){
var uri = `${apiUri}/Manga/Chapters?connector=${connector}&internalId=${internalId}&translatedLanguage=${language}`;
let json = await GetData(uri);
return json;
}
function GetCoverUrl(internalId){
return `${apiUri}/Manga/Cover?internalId=${internalId}`;
}
async function GetAllJobs(){
var uri = `${apiUri}/Jobs`;
let json = await GetData(uri);
return json;
}
async function GetRunningJobs(){
var uri = `${apiUri}/Jobs/Running`;
let json = await GetData(uri);
return json;
}
async function GetWaitingJobs(){
var uri = `${apiUri}/Jobs/Waiting`;
let json = await GetData(uri);
return json;
}
async function GetMonitorJobs(){
var uri = `${apiUri}/Jobs/MonitorJobs`;
let json = await GetData(uri);
return json;
}
async function GetProgress(jobId){
var uri = `${apiUri}/Jobs/Progress?jobId=${jobId}`;
let json = await GetData(uri);
return json;
}
async function GetSettings(){
var uri = `${apiUri}/Settings`;
let json = await GetData(uri);
return json;
}
async function GetAvailableNotificationConnectors(){
var uri = `${apiUri}/NotificationConnectors/Types`;
let json = await GetData(uri);
return json;
}
async function GetNotificationConnectors(){
var uri = `${apiUri}/NotificationConnectors`;
let json = await GetData(uri);
return json;
}
async function GetAvailableLibraryConnectors(){
var uri = `${apiUri}/LibraryConnectors/Types`;
let json = await GetData(uri);
return json;
}
async function GetLibraryConnectors(){
var uri = `${apiUri}/LibraryConnectors`;
let json = await GetData(uri);
return json;
}
function CreateMonitorJob(connector, internalId, language){
var uri = `${apiUri}/Jobs/MonitorManga?connector=${connector}&internalId=${internalId}&interval=03:00:00&translatedLanguage=${language}`;
PostData(uri);
}
function CreateDownloadNewChaptersJob(connector, internalId, language){
var uri = `${apiUri}/Jobs/DownloadNewChapters?connector=${connector}&internalId=${internalId}&translatedLanguage=${language}`;
PostData(uri);
}
function StartJob(jobId){
var uri = `${apiUri}/Jobs/StartNow?jobId=${jobId}`;
PostData(uri);
}
function UpdateDownloadLocation(downloadLocation){
var uri = `${apiUri}/Settings/UpdateDownloadLocation?downloadLocation=${downloadLocation}`;
PostData(uri);
}
function ChangeStyleSheet(sheet){
var uri = `${apiUri}/Settings/ChangeStyleSheet?styleSheet=${sheet}`;
PostData(uri);
}
function UpdateKomga(komgaUrl, komgaAuth){
var uri = `${apiUri}/LibraryConnectors/Update?libraryConnector=Komga&komgaUrl=${komgaUrl}&komgaAuth=${komgaAuth}`;
PostData(uri);
}
function UpdateKavita(kavitaUrl, kavitaUsername, kavitaPassword){
var uri = `${apiUri}/LibraryConnectors/Update?libraryConnector=Kavita&kavitaUrl=${kavitaUrl}&kavitaUsername=${kavitaUsername}&kavitaPassword=${kavitaPassword}`;
PostData(uri);
}
function UpdateGotify(gotifyUrl, gotifyAppToken){
var uri = `${apiUri}/NotificationConnectors/Update?notificationConnector=Gotify&gotifyUrl=${gotifyUrl}&gotifyAppToken=${gotifyAppToken}`;
PostData(uri);
}
function UpdateLunaSea(lunaseaWebhook){
var uri = `${apiUri}/NotificationConnectors/Update?notificationConnector=LunaSea&lunaseaWebhook=${lunaseaWebhook}`;
PostData(uri);
}
function UpdateNtfy(ntfyEndpoint, ntfyAuth){
var uri = `${apiUri}/NotificationConnectors/Update?notificationConnector=Ntfy&ntfyUrl=${ntfyEndpoint}&ntfyAuth=${ntfyAuth}`;
PostData(uri);
}
function RemoveJob(jobId){
var uri = `${apiUri}/Jobs?jobId=${jobId}`;
DeleteData(uri);
}
function CancelJob(jobId){
var uri = `${apiUri}/Jobs/Cancel?jobId=${jobId}`;
PostData(uri);
}
async function GetLogmessages(count){
var uri = `${apiUri}/LogMessages?count=${count}`;
let json = await GetData(uri);
return json;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@ -1,160 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tranga</title>
<link id='pagestyle' rel="stylesheet" href="styles/style_default.css">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<wrapper>
<topbar>
<titlebox>
<img alt="website image is Blahaj" src="media/blahaj.png">
<span>Tranga</span>
</titlebox>
<spacer></spacer>
<searchdiv>
<label for="searchbox"></label><input id="searchbox" placeholder="Filter" type="text">
</searchdiv>
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
</topbar>
<viewport>
<div id="loaderdiv">
<blur-background></blur-background>
<div id="loader"></div>
<p id="loaderText">Check your Settings > API-URI</p>
</div>
<content>
<div id="addPublication">
<p>+</p>
</div>
<publication onclick="ShowNewMangaSearch()">
<img alt="cover" src="media/cover.jpg">
<publication-information>
<connector-name class="pill">Sample</connector-name>
<publication-name>Best Manga there is</publication-name>
</publication-information>
</publication>
</content>
<popup id="newMangaPopup">
<blur-background id="blurBackgroundNewMangaPopup" onclick="newMangaPopup.style.display = 'none';"></blur-background>
<div id="newMangaPopupSelector">
<select id="newMangaConnector" />
<input type="text" placeholder="Title" id="newMangaTitle" />
<select id="newMangaTranslatedLanguage">
<option selected="selected">en</option>
<option>it</option>
<option>de</option>
</select>
</div>
<div id="newMangaResult"></div>
</popup>
<popup id="settingsPopup">
<blur-background id="blurBackgroundSettingsPopup" onclick="
settingsPopup.style.display = 'none';"></blur-background>
<popup-window>
<popup-title>Settings</popup-title>
<popup-content>
<div>
<p class="title">Download Location:</p>
<span id="downloadLocation"></span>
</div>
<div>
<p class="title">API-URI</p>
<label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri">
</div>
<div>
<span class="title">Komga</span>
<div>Configured: <span id="komgaConfigured">✅❌</span></div>
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
</div>
<div>
<span class="title">Kavita</span>
<div>Configured: <span id="kavitaConfigured">✅❌</span></div>
<label for="kavitaUrl"></label><input placeholder="URL" id="kavitaUrl" type="text">
<label for="kavitaUsername"></label><input placeholder="Username" id="kavitaUsername" type="text">
<label for="kavitaPassword"></label><input placeholder="Password" id="kavitaPassword" type="password">
</div>
<div>
<span class="title">Gotify</span>
<div>Configured: <span id="gotifyConfigured">✅❌</span></div>
<label for="gotifyUrl"></label><input placeholder="URL" id="gotifyUrl" type="text">
<label for="gotifyAppToken"></label><input placeholder="App-Token" id="gotifyAppToken" type="text">
</div>
<div>
<span class="title">LunaSea</span>
<div>Configured: <span id="lunaseaConfigured">✅❌</span></div>
<label for="lunaseaWebhook"></label><input placeholder="device/:id or user/:id" id="lunaseaWebhook" type="text">
</div>
<div>
<span class="title">Ntfy</span>
<div>Configured: <span id="ntfyConfigured">✅❌</span></div>
<label for="ntfyEndpoint"></label><input placeholder="URL" id="ntfyEndpoint" type="text">
<label for="ntfyAuth"></label><input placeholder="Auth" id="ntfyAuth" type="text">
</div>
<div>
<input type="checkbox" id="mangaHoverCheckbox" name="css-style" value="style_mangahover.css">
<label for="css-style"> Show manga titles and sources on hover</label><br>
</div>
<div>
<input type="submit" value="Update" onclick="UpdateSettings()">
</div>
</popup-content>
</popup-window>
</popup>
<popup id="publicationViewerPopup">
<blur-background id="blurBackgroundPublicationPopup" onclick="publicationViewerPopup.style.display= 'none';"></blur-background>
<publication-viewer>
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
<publication-details>
<publication-name id="publicationViewerName">Best Manga there is</publication-name>
<publication-tags id="publicationViewerTags">A Manga</publication-tags>
<publication-author id="publicationViewerAuthor">Glax</publication-author>
<publication-description id="publicationViewerDescription">
An interesting description. The description is very intriguing, yet wholesome.
</publication-description>
<publication-interactions>
<publication-starttask id="startJobButton">Start Job ▶️</publication-starttask>
<publication-canceltask id="cancelJobButton">Cancel Job ❌</publication-canceltask>
<publication-delete id="deleteJobButton">Delete Job 🗑️</publication-delete>
<publication-add id="createMonitorJobButton">Monitor </publication-add>
<publication-add id="createDownloadChapterJobButton">Download Chapter 📥</publication-add>
</publication-interactions>
</publication-details>
</publication-viewer>
</popup>
<popup id="jobStatusView">
<blur-background id="blurBackgroundJobStatus" onclick="jobStatusView.style.display= 'none';"></blur-background>
<popup-window>
<div>
<div id="jobStatusRunning" style="border-right: 1px solid gray;"></div>
</div>
<div>
<div id="jobStatusWaiting" style="border-left: 1px solid gray;"></div>
</div>
</popup-window>
</popup>
</viewport>
<footer>
<div onclick="ShowJobQueue();">
<img src="media/running.svg" alt="running"><div id="jobsRunningTag">0</div>
</div>
<div onclick="ShowJobQueue();">
<img src="media/queue.svg" alt="queue"><div id="jobsQueuedTag">0</div>
</div>
<p id="madeWith">Made with Blåhaj 🦈</p>
</footer>
</wrapper>
<script src="apiConnector.js"></script>
<script src="interaction.js"></script>
</body>
</html>

View File

@ -1,606 +0,0 @@
let monitoringJobsCount = 0;
let runningJobs = [];
let waitingJobs = [];
let notificationConnectorTypes = [];
let libraryConnectorTypes = [];
let selectedManga;
let selectedJob;
const searchBox = document.querySelector("#searchbox");
const settingsPopup = document.querySelector("#settingsPopup");
const settingsCog = document.querySelector("#settingscog");
const tasksContent = document.querySelector("content");
const createMonitorTaskButton = document.querySelector("#createMonitoJobButton");
const createDownloadChapterTaskButton = document.querySelector("#createDownloadChapterJobButton");
const startJobButton = document.querySelector("#startJobButton");
const cancelJobButton = document.querySelector("#cancelJobButton");
const deleteJobButton = document.querySelector("#deleteJobButton");
const mangaViewerPopup = document.querySelector("#publicationViewerPopup");
const mangaViewerWindow = document.querySelector("publication-viewer");
const mangaViewerDescription = document.querySelector("#publicationViewerDescription");
const mangaViewerName = document.querySelector("#publicationViewerName");
const mangaViewerTags = document.querySelector("#publicationViewerTags");
const mangaViewerAuthor = document.querySelector("#publicationViewerAuthor");
const mangaViewCover = document.querySelector("#pubviewcover");
const settingDownloadLocation = document.querySelector("#downloadLocation");
const settingKomgaUrl = document.querySelector("#komgaUrl");
const settingKomgaUser = document.querySelector("#komgaUsername");
const settingKomgaPass = document.querySelector("#komgaPassword");
const settingKavitaUrl = document.querySelector("#kavitaUrl");
const settingKavitaUser = document.querySelector("#kavitaUsername");
const settingKavitaPass = document.querySelector("#kavitaPassword");
const settingGotifyUrl = document.querySelector("#gotifyUrl");
const settingGotifyAppToken = document.querySelector("#gotifyAppToken");
const settingLunaseaWebhook = document.querySelector("#lunaseaWebhook");
const settingNtfyEndpoint = document.querySelector("#ntfyEndpoint");
const settingNtfyAuth = document.querySelector("#ntfyAuth");
const settingKomgaConfigured = document.querySelector("#komgaConfigured");
const settingKavitaConfigured = document.querySelector("#kavitaConfigured");
const settingGotifyConfigured = document.querySelector("#gotifyConfigured");
const settingLunaseaConfigured = document.querySelector("#lunaseaConfigured");
const settingNtfyConfigured = document.querySelector("#ntfyConfigured");
const settingApiUri = document.querySelector("#settingApiUri");
const settingMangaHoverCheckbox = document.querySelector("#mangaHoverCheckbox");
const newMangaPopup = document.querySelector("#newMangaPopup");
const newMangaConnector = document.querySelector("#newMangaConnector");
const newMangaTitle = document.querySelector("#newMangaTitle");
const newMangaResult = document.querySelector("#newMangaResult");
const newMangaTranslatedLanguage = document.querySelector("#newMangaTranslatedLanguage");
const jobsRunningTag = document.querySelector("#jobsRunningTag");
const jobsQueuedTag = document.querySelector("#jobsQueuedTag");
const loaderdiv = document.querySelector('#loaderdiv');
const jobStatusView = document.querySelector("#jobStatusView");
const jobStatusRunning = document.querySelector("#jobStatusRunning");
const jobStatusWaiting = document.querySelector("#jobStatusWaiting");
function Setup(){
Ping().then((ret) => {
loaderdiv.style.display = 'none';
GetAvailableNotificationConnectors().then((json) => {
json.forEach(connector => {
notificationConnectorTypes[connector.Key] = connector.Value;
});
});
GetAvailableLibraryConnectors().then((json) => {
json.forEach(connector => {
libraryConnectorTypes[connector.Key] = connector.Value;
});
});
GetAvailableControllers().then((json) => {
newMangaConnector.replaceChildren();
json.forEach(connector => {
var option = document.createElement('option');
option.value = connector;
option.innerText = connector;
newMangaConnector.appendChild(option);
});
});
ResetContent();
UpdateJobs();
GetSettings().then((json) => {
//console.log(json);
settingDownloadLocation.innerText = json.downloadLocation;
settingApiUri.placeholder = apiUri;
//console.log(json.styleSheet);
if (json.styleSheet == 'hover') {
settingMangaHoverCheckbox.checked = true;
document.getElementById('pagestyle').setAttribute('href', 'styles/style_mangahover.css');
} else {
settingMangaHoverCheckbox.checked = false;
document.getElementById('pagestyle').setAttribute('href', 'styles/style_default.css');
}
});
setInterval(() => {
UpdateJobs();
}, 1000);
});
}
Setup();
function ResetContent(){
//Delete everything
tasksContent.replaceChildren();
//Add "Add new Task" Button
var add = document.createElement("div");
add.setAttribute("id", "addPublication")
var plus = document.createElement("p");
plus.innerText = "+";
add.appendChild(plus);
add.addEventListener("click", () => ShowNewMangaSearch());
tasksContent.appendChild(add);
//Populate with the monitored mangas
GetMonitorJobs().then((json) => {
//console.log(json);
json.forEach(job => {
var mangaView = CreateManga(job.manga, job.mangaConnector.name);
mangaView.addEventListener("click", (event) => {
ShowMangaWindow(job, job.manga, event, false);
});
tasksContent.appendChild(mangaView);
});
monitoringJobsCount = json.length;
});
}
function ShowNewMangaSearch(){
newMangaTitle.value = "";
newMangaPopup.style.display = "block";
newMangaResult.replaceChildren();
}
newMangaTitle.addEventListener("keypress", (event) => { if(event.key === "Enter") GetNewMangaItems();})
function GetNewMangaItems(){
if(newMangaTitle.value.length < 4)
return;
newMangaResult.replaceChildren();
newMangaConnector.disabled = true;
newMangaTitle.disabled = true;
newMangaTranslatedLanguage.disabled = true;
GetPublicationFromConnector(newMangaConnector.value, newMangaTitle.value).then((json) => {
//console.log(json);
if(json.length > 0)
newMangaResult.style.display = "flex";
json.forEach(result => {
var mangaElement = CreateManga(result, newMangaConnector.value)
newMangaResult.appendChild(mangaElement);
mangaElement.addEventListener("click", (event) => {
ShowMangaWindow(null, result, event, true);
});
});
newMangaConnector.disabled = false;
newMangaTitle.disabled = false;
newMangaTranslatedLanguage.disabled = false;
});
}
//Returns a new "Publication" Item to display in the jobs section
function CreateManga(manga, connector){
//Create a new publication and set an internal ID
var mangaElement = document.createElement('publication');
mangaElement.id = GetValidSelector(manga.internalId);
//Append the cover image to the publication
var mangaImage = document.createElement('img');
mangaImage.src = GetCoverUrl(manga.internalId);
mangaElement.appendChild(mangaImage);
//Append the publication information to the publication
//console.log(manga);
var info = document.createElement('publication-information');
var connectorName = document.createElement('connector-name');
connectorName.innerText = connector;
connectorName.className = "pill";
connectorName.style.backgroundColor = stringToColour(connector);
info.appendChild(connectorName);
var mangaName = document.createElement('publication-name');
mangaName.innerText = manga.sortName;
//Create the publication status indicator
var releaseStatus = document.createElement('publication-status');
var statusTooltip = document.createElement('status-tooltip');
if (manga.releaseStatus == 0) {
releaseStatus.style.backgroundColor = 'limegreen';
statusTooltip.innerText = "Ongoing"
}else if(manga.releaseStatus == 1){
releaseStatus.style.backgroundColor = 'blueviolet';
statusTooltip.innerText = "Completed"
} else if (manga.releaseStatus == 2) {
releaseStatus.style.backgroundColor = 'darkorange';
statusTooltip.innerText = "On Hiatus"
} else if (manga.releaseStatus == 3) {
releaseStatus.style.backgroundColor = 'firebrick';
statusTooltip.innerText = "Cancelled"
} else if (manga.releaseStatus == 4) {
releaseStatus.style.backgroundColor = 'aqua';
statusTooltip.innerText = "Upcoming";
} else {
releaseStatus.style.backgroundColor = 'gray';
statusTooltip.innerText = 'Status Unavailable';
}
releaseStatus.appendChild(statusTooltip); //Append the tooltip to the indicator circle
mangaName.appendChild(releaseStatus); //Append the release status indicator to the info block
info.appendChild(mangaName);
mangaElement.appendChild(info);
return mangaElement;
}
createMonitorJobButton.addEventListener("click", () => {
CreateMonitorJob(newMangaConnector.value, selectedManga.internalId, newMangaTranslatedLanguage.value);
UpdateJobs();
mangaViewerPopup.style.display = "none";
ResetContent();
});
startJobButton.addEventListener("click", () => {
StartJob(selectedJob.id);
mangaViewerPopup.style.display = "none";
});
cancelJobButton.addEventListener("click", () => {
CancelJob(selectedJob.id);
mangaViewerPopup.style.display = "none";
});
deleteJobButton.addEventListener("click", () => {
RemoveJob(selectedJob.id);
UpdateJobs();
mangaViewerPopup.style.display = "none";
ResetContent();
});
function ShowMangaWindow(job, manga, event, add){
selectedManga = manga;
selectedJob = job;
//Show popup
mangaViewerPopup.style.display = "block";
//Set position to mouse-position
if(event.clientY < window.innerHeight - mangaViewerWindow.offsetHeight)
mangaViewerWindow.style.top = `${event.clientY}px`;
else
mangaViewerWindow.style.top = `${event.clientY - mangaViewerWindow.offsetHeight}px`;
if(event.clientX < window.innerWidth - mangaViewerWindow.offsetWidth)
mangaViewerWindow.style.left = `${event.clientX}px`;
else
mangaViewerWindow.style.left = `${event.clientX - mangaViewerWindow.offsetWidth}px`;
//Edit information inside the window
mangaViewerName.innerText = manga.sortName;
mangaViewerTags.innerText = manga.tags.join(", ");
mangaViewerDescription.innerText = manga.description;
mangaViewerAuthor.innerText = manga.authors.join(',');
mangaViewCover.src = GetCoverUrl(manga.internalId);
toEditId = manga.internalId;
//Check what action should be listed
if(add){
createMonitorJobButton.style.display = "initial";
createDownloadChapterJobButton.style.display = "none";
cancelJobButton.style.display = "none";
startJobButton.style.display = "none";
deleteJobButton.style.display = "none";
}
else{
createMonitorJobButton.style.display = "none";
createDownloadChapterJobButton.style.display = "none";
cancelJobButton.style.display = "initial";
startJobButton.style.display = "initial";
deleteJobButton.style.display = "initial";
}
}
function HidePublicationPopup(){
publicationViewerPopup.style.display = "none";
}
searchBox.addEventListener("keyup", () => FilterResults());
//Filter shown jobs
function FilterResults(){
if(searchBox.value.length > 0){
tasksContent.childNodes.forEach(publication => {
publication.childNodes.forEach(item => {
if(item.nodeName.toLowerCase() == "publication-information"){
item.childNodes.forEach(information => {
if(information.nodeName.toLowerCase() == "publication-name"){
if(!information.textContent.toLowerCase().includes(searchBox.value.toLowerCase())){
publication.style.display = "none";
}else{
publication.style.display = "initial";
}
}
});
}
});
});
}else{
tasksContent.childNodes.forEach(publication => publication.style.display = "initial");
}
}
settingsCog.addEventListener("click", () => {
OpenSettings();
settingsPopup.style.display = "flex";
});
settingKomgaUrl.addEventListener("keypress", (event) => { { if(event.key === "Enter") UpdateSettings(); } });
settingKomgaUser.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
settingKomgaPass.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
settingKavitaUrl.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
settingKavitaUser.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
settingKavitaPass.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
settingGotifyUrl.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
settingGotifyAppToken.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
settingLunaseaWebhook.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
settingNtfyEndpoint.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
settingNtfyAuth.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
settingApiUri.addEventListener("keypress", (event) => { if(event.key === "Enter") UpdateSettings(); });
function OpenSettings(){
settingGotifyConfigured.innerText = "❌";
settingLunaseaConfigured.innerText = "❌";
settingNtfyConfigured.innerText = "❌";
settingKavitaConfigured.innerText = "❌";
settingKomgaConfigured.innerText = "❌";
settingKomgaUrl.value = "";
settingKomgaUser.value = "";
settingKomgaPass.value = "";
settingKavitaUrl.value = "";
settingKavitaUser.value = "";
settingKavitaPass.value = "";
settingGotifyUrl.value = "";
settingGotifyAppToken.value = "";
settingLunaseaWebhook.value = "";
settingNtfyAuth.value = "";
settingNtfyEndpoint.value = "";
settingApiUri.value = "";
settingMangaHoverCheckbox.checked = false;
GetSettings().then((json) => {
//console.log(json);
settingDownloadLocation.innerText = json.downloadLocation;
settingApiUri.placeholder = apiUri;
//console.log(json.styleSheet);
if (json.styleSheet == 'hover') {
settingMangaHoverCheckbox.checked = true;
document.getElementById('pagestyle').setAttribute('href', 'styles/style_mangahover.css');
} else {
settingMangaHoverCheckbox.checked = false;
document.getElementById('pagestyle').setAttribute('href', 'styles/style_default.css');
}
});
GetLibraryConnectors().then((json) => {
//console.log(json);
json.forEach(connector => {
switch(libraryConnectorTypes[connector.libraryType]){
case "Kavita":
settingKavitaConfigured.innerText = "✅";
settingKavitaUrl.placeholder = connector.baseUrl;
settingKavitaUser.placeholder = "***";
settingKavitaPass.placeholder = "***";
break;
case "Komga":
settingKomgaConfigured.innerText = "✅";
settingKomgaUrl.placeholder = connector.baseUrl;
settingKomgaUser.placeholder = "***";
settingKomgaPass.placeholder = "***";
break;
default:
console.log("Unknown type");
console.log(connector);
break;
}
});
});
GetNotificationConnectors().then((json) => {
//console.log(json);
json.forEach(connector => {
switch(notificationConnectorTypes[connector.notificationConnectorType]){
case "Gotify":
settingGotifyUrl.placeholder = connector.endpoint;
settingGotifyAppToken.placeholder = "***";
settingGotifyConfigured.innerText = "✅";
break;
case "LunaSea":
settingLunaseaConfigured.innerText = "✅";
settingLunaseaWebhook.placeholder = connector.id;
break;
case "Ntfy":
settingNtfyConfigured.innerText = "✅";
settingNtfyEndpoint.placeholder = connector.endpoint;
settingNtfyAuth.placeholder = "***";
break;
default:
console.log("Unknown type");
console.log(connector);
break;
}
});
});
}
function UpdateSettings(){
if(settingApiUri.value != ""){
apiUri = settingApiUri.value;
setCookie("apiUri", apiUri);
Setup();
}
// If the checkbox is checked, set the style to style_mangahover.css and
if (document.getElementById("mangaHoverCheckbox").checked == true){
ChangeStyleSheet('hover')
//console.log('Changing theme to mangahover')
} else {
ChangeStyleSheet('default');
//console.log('Changing theme to default')
}
if(settingKomgaUrl.value != "" &&
settingKomgaUser.value != "" &&
settingKomgaPass.value != ""){
UpdateKomga(settingKomgaUrl.value, utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`));
}
if(settingKavitaUrl.value != "" &&
settingKavitaUser.value != "" &&
settingKavitaPass.value != ""){
UpdateKavita(settingKavitaUrl.value, settingKavitaUser.value, settingKavitaPass.value);
}
if(settingGotifyUrl.value != "" &&
settingGotifyAppToken.value != ""){
UpdateGotify(settingGotifyUrl.value, settingGotifyAppToken.value);
}
if(settingLunaseaWebhook.value != ""){
UpdateLunaSea(settingLunaseaWebhook.value);
}
if(settingNtfyEndpoint.value != "" &&
settingNtfyAuth.value != ""){
UpdateNtfy(settingNtfyEndpoint.value, settingNtfyAuth.value);
}
OpenSettings();
Setup();
}
function utf8_to_b64(str) {
return window.btoa(unescape(encodeURIComponent( str )));
}
function UpdateJobs(){
GetMonitorJobs().then((json) => {
if(monitoringJobsCount != json.length){
ResetContent();
//console.log(json);
json.forEach(job => {
var mangaView = CreateManga(job.manga, job.mangaConnector.name);
mangaView.addEventListener("click", (event) => {
ShowMangaWindow(job, job.manga, event, false);
});
tasksContent.appendChild(mangaView);
});
monitoringJobsCount = json.length;
}
});
//Get the jobs that are waiting in the queue
GetWaitingJobs().then((json) => {
jobsQueuedTag.innerText = json.length;
var nowWaitingJobs = [];
json.forEach(job => {
if(!waitingJobs.includes(GetValidSelector(job.id))){
var jobDom = createJob(job);
jobStatusWaiting.appendChild(jobDom);
}
nowWaitingJobs.push(GetValidSelector(job.id));
});
waitingJobs = nowWaitingJobs;
});
jobStatusWaiting.childNodes.forEach(child => {
if(!waitingJobs.includes(child.id))
jobStatusWaiting.removeChild(child);
});
//Get currently running jobs
GetRunningJobs().then((json) => {
jobsRunningTag.innerText = json.length;
var nowRunningJobs = [];
json.forEach(job => {
if(!runningJobs.includes(GetValidSelector(job.id))){
var jobDom = createJob(job);
jobStatusRunning.appendChild(jobDom);
}
nowRunningJobs.push(GetValidSelector(job.id));
UpdateJobProgress(job.id);
});
runningJobs = nowRunningJobs;
});
jobStatusRunning.childNodes.forEach(child => {
if(!runningJobs.includes(child.id))
jobStatusRunning.removeChild(child);
});
}
function createJob(jobjson){
var manga;
if(jobjson.chapter != null)
manga = jobjson.chapter.parentManga;
else if(jobjson.manga != null)
manga = jobjson.manga;
else return null;
var wrapper = document.createElement("div");
wrapper.className = "jobWrapper";
wrapper.id = GetValidSelector(jobjson.id);
var image = document.createElement("img");
image.className = "jobImage";
image.src = GetCoverUrl(manga.internalId);
wrapper.appendChild(image);
var title = document.createElement("span");
title.className = "jobTitle";
if(jobjson.chapter != null)
title.innerText = `${manga.sortName} - ${jobjson.chapter.fileName}`;
else if(jobjson.manga != null)
title.innerText = manga.sortName;
wrapper.appendChild(title);
var progressBar = document.createElement("progress");
progressBar.className = "jobProgressBar";
progressBar.id = `jobProgressBar${GetValidSelector(jobjson.id)}`;
wrapper.appendChild(progressBar);
var progressSpan = document.createElement("span");
progressSpan.className = "jobProgressSpan";
progressSpan.id = `jobProgressSpan${GetValidSelector(jobjson.id)}`;
progressSpan.innerText = "0% 00:00:00";
wrapper.appendChild(progressSpan);
var cancelSpan = document.createElement("span");
cancelSpan.className = "jobCancel";
cancelSpan.innerText = "Cancel";
cancelSpan.addEventListener("click", () => CancelJob(jobjson.id));
wrapper.appendChild(cancelSpan);
return wrapper;
}
function ShowJobQueue(){
jobStatusView.style.display = "initial";
}
function UpdateJobProgress(jobId){
GetProgress(jobId).then((json) => {
var progressBar = document.querySelector(`#jobProgressBar${GetValidSelector(jobId)}`);
var progressSpan = document.querySelector(`#jobProgressSpan${GetValidSelector(jobId)}`);
if(progressBar != null && json.progress != 0){
progressBar.value = json.progress;
}
if(progressSpan != null){
var percentageStr = "0%";
var timeleftStr = "00:00:00";
if(json.progress != 0){
percentageStr = Intl.NumberFormat("en-US", { style: "percent"}).format(json.progress);
timeleftStr = json.timeRemaining.split('.')[0];
}
progressSpan.innerText = `${percentageStr} ${timeleftStr}`;
}
});
}
function GetValidSelector(str){
var clean = [...str.matchAll(/[a-zA-Z0-9]*-*_*/g)];
return clean.join('');
}
const stringToColour = (str) => {
let hash = 0;
str.split('').forEach(char => {
hash = char.charCodeAt(0) + ((hash << 5) - hash)
})
let colour = '#'
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff
colour += value.toString(16).padStart(2, '0')
}
return colour
}

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="#0F1729"/>
</svg>

Before

Width:  |  Height:  |  Size: 804 B

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
<g fill="#000000">

Before

Width:  |  Height:  |  Size: 545 B

View File

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 235.504 235.504"
xml:space="preserve">
<g>
<g>
<path d="M195.209,81.456l-49.227-0.15c0.737-0.886,1.351-1.868,2.284-2.583c3.282-2.497,3.911-7.166,1.427-10.438
c-2.501-3.266-7.161-3.919-10.443-1.423c-4.873,3.715-8.388,8.704-10.255,14.389l-22.191-0.064
c-9.508,0-19.588,7.398-22.938,16.851l-16.877,47.479c-1.775,5.013-1.338,9.966,1.207,13.568
c2.412,3.427,6.384,5.318,11.187,5.358l45.126,0.136c-1.509,5.186-4.701,9.622-9.352,12.424
c-4.891,2.957-10.636,3.814-16.172,2.444c-3.994-0.998-8.031,1.442-9.027,5.418c-0.99,4.012,1.445,8.035,5.432,9.032
c2.927,0.738,5.879,1.091,8.808,1.091c6.516,0,12.93-1.788,18.645-5.23c8.312-5.013,14.172-12.979,16.484-22.409
c0.232-0.905,0.232-1.823,0.124-2.713l28.296,0.092h0.049c2.925,0,5.854-0.89,8.684-2.147c0.2,0.493,0.32,1.014,0.661,1.471
c3.335,4.677,4.629,10.343,3.688,15.993c-0.95,5.627-4.028,10.536-8.688,13.862c-3.351,2.376-4.14,7.037-1.755,10.379
c1.466,2.04,3.751,3.122,6.062,3.122c1.491,0,3.006-0.429,4.312-1.367c7.919-5.61,13.16-13.966,14.771-23.52
c1.603-9.565-0.613-19.203-6.28-27.122c-0.48-0.693-1.134-1.19-1.779-1.659c1.318-1.831,2.501-3.763,3.238-5.854l16.863-47.464
c1.795-5.018,1.351-9.969-1.194-13.58C203.954,83.387,200.015,81.47,195.209,81.456z M201.979,98.405l-16.868,47.464
c-0.981,2.757-2.941,5.214-5.213,7.329c-0.337,0.16-0.706,0.229-1.026,0.465c-0.673,0.485-1.182,1.122-1.639,1.747
c-2.962,1.996-6.288,3.339-9.434,3.339v2.989l-0.044-2.989l-33.194-0.101c-0.232-0.076-0.424-0.261-0.661-0.324
c-1.435-0.353-2.805-0.145-4.095,0.309l-29.768-0.101l1.192-3.358c0.549-1.547-0.269-3.25-1.813-3.795
c-1.521-0.553-3.25,0.24-3.799,1.804l-1.899,5.334l-14.318-0.044c-2.805,0-5.063-0.998-6.336-2.813
c-1.437-2.032-1.603-4.921-0.463-8.144l16.877-47.478c2.48-6.979,10.417-12.868,17.356-12.868l12.217,0.038l-1.963,5.536
c-0.555,1.549,0.262,3.25,1.805,3.797c0.331,0.12,0.661,0.174,0.998,0.174c1.227,0,2.372-0.768,2.793-1.986l2.497-7.019
c0.064-0.164-0.048-0.322-0.016-0.487h2.512c-0.905,7.758,1.163,15.42,5.947,21.638c5.903,7.687,14.852,11.726,23.873,11.726
c6.371,0,12.771-2.001,18.186-6.129c3.266-2.488,3.911-7.167,1.426-10.441c-2.508-3.267-7.161-3.901-10.455-1.415
c-6.612,5.056-16.146,3.775-21.223-2.809c-2.445-3.194-3.487-7.133-2.958-11.117c0.061-0.503,0.353-0.916,0.481-1.402
l52.216,0.156c2.806,0,5.054,1.004,6.324,2.811C202.928,92.241,203.105,95.223,201.979,98.405z"/>
<path d="M107.997,127.194c-1.531-0.553-3.248,0.244-3.799,1.791l-4.302,12.099c-0.551,1.543,0.265,3.242,1.813,3.795
c0.331,0.116,0.659,0.16,0.998,0.16c1.214,0,2.372-0.765,2.801-1.976l4.294-12.099
C110.369,129.446,109.551,127.728,107.997,127.194z"/>
<path d="M116.6,103.014c-1.529-0.541-3.25,0.252-3.805,1.805l-4.298,12.088c-0.547,1.547,0.261,3.252,1.799,3.799
c0.329,0.12,0.659,0.172,1,0.172c1.222,0,2.368-0.769,2.809-1.983l4.294-12.09C118.955,105.268,118.139,103.555,116.6,103.014z"/>
<path d="M232.527,90.428l-14.896-0.038l0,0c-1.639,0-2.974,1.327-2.997,2.976c0,1.639,1.342,2.981,2.981,2.989l14.896,0.042l0,0
c1.643,0,2.978-1.331,2.993-2.979C235.504,91.763,234.17,90.436,232.527,90.428z"/>
<path d="M220.333,80.436c0.629,0,1.242-0.188,1.771-0.583l11.994-8.83c1.326-0.974,1.611-2.842,0.645-4.168
c-0.965-1.327-2.845-1.611-4.163-0.637l-11.998,8.833c-1.323,0.974-1.607,2.841-0.642,4.167
C218.513,80.003,219.418,80.436,220.333,80.436z"/>
<path d="M209.152,56.279c-1.547-0.549-3.25,0.269-3.787,1.805l-4.997,14.036c-0.537,1.547,0.26,3.252,1.803,3.807
c0.337,0.12,0.674,0.172,0.994,0.172c1.242,0,2.385-0.757,2.821-1.986l4.985-14.036C211.516,58.541,210.695,56.846,209.152,56.279
z"/>
<path d="M17.587,100.894h55.208c1.641,0,2.976-1.343,2.976-2.981c0-1.641-1.334-2.988-2.976-2.988H17.587
c-1.641,0-2.988,1.338-2.988,2.988C14.599,99.559,15.946,100.894,17.587,100.894z"/>
<path d="M68.471,119.328c0-1.641-1.345-2.987-2.986-2.987H10.283c-1.639,0-2.981,1.338-2.981,2.987
c0,1.639,1.342,2.974,2.981,2.974h55.202C67.119,122.301,68.471,120.967,68.471,119.328z"/>
<path d="M58.188,137.758H2.974c-1.641,0-2.974,1.335-2.974,2.989c0,1.64,1.333,2.974,2.974,2.974h55.214
c1.639,0,2.981-1.334,2.981-2.974C61.162,139.093,59.827,137.758,58.188,137.758z"/>
<path d="M169.611,28.097c11.821,0,21.403,9.584,21.403,21.41c0,11.82-9.582,21.408-21.403,21.408
c-11.822,0-21.412-9.588-21.412-21.408C148.199,37.681,157.789,28.097,169.611,28.097z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 93.5 93.5" xml:space="preserve">
<g>
<g>
<path d="M93.5,40.899c0-2.453-1.995-4.447-4.448-4.447H81.98c-0.74-2.545-1.756-5.001-3.035-7.331l4.998-5
c0.826-0.827,1.303-1.973,1.303-3.146c0-1.19-0.462-2.306-1.303-3.146L75.67,9.555c-1.613-1.615-4.673-1.618-6.29,0l-5,5
c-2.327-1.28-4.786-2.296-7.332-3.037v-7.07C57.048,1.995,55.053,0,52.602,0H40.899c-2.453,0-4.447,1.995-4.447,4.448v7.071
c-2.546,0.741-5.005,1.757-7.333,3.037l-5-5c-1.68-1.679-4.609-1.679-6.288,0L9.555,17.83c-1.734,1.734-1.734,4.555,0,6.289
l4.999,5c-1.279,2.33-2.295,4.788-3.036,7.333h-7.07C1.995,36.452,0,38.447,0,40.899V52.6c0,2.453,1.995,4.447,4.448,4.447h7.071
c0.74,2.545,1.757,5.003,3.036,7.332l-4.998,4.999c-0.827,0.827-1.303,1.974-1.303,3.146c0,1.189,0.462,2.307,1.302,3.146
l8.274,8.273c1.614,1.615,4.674,1.619,6.29,0l5-5c2.328,1.279,4.786,2.297,7.333,3.037v7.071c0,2.453,1.995,4.448,4.447,4.448
h11.702c2.453,0,4.446-1.995,4.446-4.448V81.98c2.546-0.74,5.005-1.756,7.332-3.037l5,5c1.681,1.68,4.608,1.68,6.288,0
l8.275-8.273c1.734-1.734,1.734-4.555,0-6.289l-4.998-5.001c1.279-2.329,2.295-4.787,3.035-7.332h7.071
c2.453,0,4.448-1.995,4.448-4.446V40.899z M62.947,46.75c0,8.932-7.266,16.197-16.197,16.197c-8.931,0-16.197-7.266-16.197-16.197
c0-8.931,7.266-16.197,16.197-16.197C55.682,30.553,62.947,37.819,62.947,46.75z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g id="task">
<path d="M4,23.4l-3.7-3.7l1.4-1.4L4,20.6l4.3-4.3l1.4,1.4L4,23.4z M24,21H12v-2h12V21z M4,15.4l-3.7-3.7l1.4-1.4L4,12.6l4.3-4.3
l1.4,1.4L4,15.4z M24,13H12v-2h12V13z M4,7.4L0.3,3.7l1.4-1.4L4,4.6l4.3-4.3l1.4,1.4L4,7.4z M24,5H12V3h12V5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 603 B

View File

@ -1,696 +0,0 @@
:root{
--background-color: #030304;
--second-background-color: #fff;
--primary-color: #f5a9b8;
--secondary-color: #5bcefa;
--accent-color: #fff;
--topbar-height: 60px;
box-sizing: border-box;
}
body{
padding: 0;
margin: 0;
height: 100vh;
background-color: var(--background-color);
font-family: "Inter", sans-serif;
overflow-x: hidden;
}
wrapper {
display: flex;
flex-flow: column;
flex-wrap: nowrap;
height: 100vh;
}
background-placeholder{
background-color: var(--second-background-color);
opacity: 1;
position: absolute;
width: 100%;
height: 100%;
border-radius: 0 0 5px 0;
z-index: -1;
}
topbar {
display: flex;
align-items: center;
height: var(--topbar-height);
background-color: var(--secondary-color);
z-index: 100;
box-shadow: 0 0 20px black;
}
titlebox {
position: relative;
display: flex;
margin: 0 0 0 40px;
height: 100%;
align-items:center;
justify-content:center;
}
titlebox span{
cursor: default;
font-size: 24pt;
font-weight: bold;
background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-left: 20px;
}
titlebox img {
height: 100%;
margin-right: 10px;
cursor: grab;
}
spacer{
flex-grow: 1;
}
searchdiv{
display: block;
margin: 0 10px 0 0;
}
#searchbox {
padding: 3px 10px;
border: 0;
border-radius: 4px;
font-size: 14pt;
width: 250px;
}
#settingscog {
cursor: pointer;
margin: 0px 30px;
height: 50%;
filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%);
}
viewport {
position: relative;
display: flex;
flex-flow: row;
flex-wrap: nowrap;
flex-grow: 1;
height: 100%;
overflow-y: scroll;
}
footer {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 100%;
height: 40px;
align-items: center;
justify-content: center;
background-color: var(--primary-color);
align-content: center;
}
footer > div {
height: 100%;
margin: 0 30px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
cursor: pointer;
}
footer > div > *{
height: 40%;
margin: 0 5px;
}
#madeWith {
flex-grow: 1;
text-align: right;
margin-right: 20px;
cursor: url("media/blahaj.png"), grab;
}
content {
position: relative;
flex-grow: 1;
border-radius: 5px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
align-content: start;
}
#settingsPopup{
z-index: 300;
}
#settingsPopup popup-content{
flex-direction: column;
align-items: start;
margin: 15px 10px;
}
#settingsPopup popup-content > * {
margin: 5px 10px;
}
#settingsPopup popup-content .title {
font-weight: bolder;
}
#addPublication {
cursor: pointer;
background-color: var(--secondary-color);
width: 180px;
height: 300px;
border-radius: 5px;
margin: 10px 10px;
padding: 15px 20px;
position: relative;
}
#addPublication p{
width: 100%;
text-align: center;
font-size: 150pt;
vertical-align: middle;
line-height: 300px;
margin: 0;
color: var(--accent-color);
}
.pill {
flex-grow: 0;
height: 14pt;
font-size: 12pt;
border-radius: 9pt;
background-color: var(--primary-color);
padding: 2pt 17px;
color: black;
}
publication{
cursor: pointer;
background-color: var(--secondary-color);
width: 180px;
height: 300px;
border-radius: 5px;
margin: 10px 10px;
padding: 15px 20px;
position: relative;
flex-shrink: 0;
}
publication::after{
content: '';
position: absolute;
left: 0; top: 0;
border-radius: 5px;
width: 100%; height: 100%;
background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
}
publication-information {
display: flex;
flex-direction: column;
justify-content: start;
}
publication-details {
display: flex;
flex-direction: column;
justify-content: start;
}
publication-information * {
z-index: 1;
color: var(--accent-color);
}
publication-details * {
z-index: 1;
color: var(--accent-color);
}
connector-name{
width: fit-content;
margin: 10px 0;
}
publication-name{
width: fit-content;
font-size: 16pt;
font-weight: bold;
}
publication-status {
display: inline-block;
height: 10px;
width: 10px;
border-radius: 50%;
margin: 5px;
position: relative;
vertical-align: middle;
}
publication-status:hover > status-tooltip {
visibility: visible;
}
status-tooltip {
visibility: hidden;
position: absolute;
/*Position*/
left: -50px ;
bottom: 150%;
z-index: 3;
/*Text Properties*/
font-size:10pt;
font-weight:bold;
color:white;
text-align: center;
padding: 5px 5px;
/*Size*/
width:100px;
background-color: black;
border-radius: 6px;
}
status-tooltip::after {
content: " ";
position: absolute;
top: 100%; /* At the bottom of the tooltip */
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: black transparent transparent transparent;
}
publication img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
border-radius: 5px;
}
popup{
display: none;
width: 100%;
min-height: 100%;
top: 0;
left: 0;
position: fixed;
z-index: 2;
flex-direction: column;
}
popup popup-window {
position: absolute;
z-index: 3;
left: 25%;
top: 100px;
width: 50%;
display: flex;
flex-direction: column;
background-color: var(--second-background-color);
border-radius: 3px;
overflow: hidden;
}
popup popup-window popup-title {
height: 30px;
font-size: 14pt;
font-weight: bolder;
padding: 5px 10px;
margin: 0;
display: flex;
align-items: center;
background-color: var(--primary-color);
color: var(--accent-color)
}
popup popup-window popup-content{
margin: 15px 10px;
display: flex;
align-items: center;
justify-content: space-evenly;
}
popup popup-window popup-content div > * {
margin: 2px 3px 0 0;
}
popup popup-window popup-content input, select {
padding: 3px 4px;
width: 130px;
border: 1px solid lightgrey;
background-color: var(--accent-color);
border-radius: 3px;
}
#newMangaPopup > div {
z-index: 3;
position: relative;
}
#newMangaPopup > #newMangaPopupSelector {
width: 600px;
height: 40px;
margin: 80px auto 0;
}
#newMangaPopup > div > #newMangaConnector, #newMangaTitle, #newMangaTranslatedLanguage {
margin: 0;
display: inline-block;
height: 40px;
}
#newMangaPopup #newMangaConnector {
width: 100px;
padding: 0 0 0 5px;
border-radius: 5px 0 0 5px;
border: 0;
border-right: 1px solid darkgray;
}
#newMangaPopup #newMangaTitle{
width: 445px;
padding: 0 5px 0 5px;
border: 0;
}
#newMangaPopup #newMangaTranslatedLanguage {
width: 45px;
border-radius: 0 5px 5px 0;
border: 0;
border-left: 1px solid darkgray;
margin-left: -5px;
}
#newMangaResult {
display: none;
flex-direction: row;
justify-content: flex-start;
margin: 5px auto 0;
border-radius: 5px;
padding: 5px;
width: min-content;
max-width: 98%;
max-height: 400px;
overflow-x: scroll;
overflow-y: hidden;
}
blur-background {
width: 100%;
height: 100%;
position: absolute;
left: 0;
background: rgba(245, 169, 184, 0.58);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(4.5px);
-webkit-backdrop-filter: blur(4.5px);
}
#publicationViewerPopup{
z-index: 5;
}
publication-viewer{
display: block;
width: 460px;
position: absolute;
top: 200px;
left: 400px;
background-color: var(--accent-color);
border-radius: 5px;
overflow: hidden;
padding: 15px;
}
publication-viewer::after{
content: '';
position: absolute;
left: 0; top: 0;
border-radius: 5px;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
backdrop-filter: blur(3px);
}
publication-viewer img {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
object-fit: cover;
border-radius: 5px;
z-index: 0;
}
publication-viewer publication-details > * {
margin: 5px 0;
}
publication-viewer publication-details publication-name {
width: initial;
overflow-x: scroll;
white-space: nowrap;
scrollbar-width: none;
}
publication-viewer publication-details publication-tags::before {
content: "Tags";
display: block;
font-weight: bolder;
}
publication-viewer publication-details publication-tags {
overflow-x: scroll;
white-space: nowrap;
scrollbar-width: none;
}
publication-viewer publication-details publication-author::before {
content: "Author: ";
font-weight: bolder;
}
publication-viewer publication-details publication-description::before {
content: "Description";
display: block;
font-weight: bolder;
}
publication-viewer publication-details publication-description {
font-size: 12pt;
margin: 5px 0;
height: 145px;
overflow-x: scroll;
}
publication-viewer publication-details publication-interactions {
display: flex;
flex-direction: row;
justify-content: end;
align-items: start;
width: 100%;
}
publication-viewer publication-details publication-interactions > * {
margin: 0 10px;
font-size: 16pt;
cursor: pointer;
}
publication-viewer publication-details publication-interactions publication-starttask {
color: var(--secondary-color);
}
publication-viewer publication-details publication-interactions publication-delete {
color: red;
}
publication-view publication-details publication-interactions publication-canceltask {
color: yellow;
}
publication-viewer publication-details publication-interactions publication-add {
color: limegreen;
}
footer-tag-popup {
display: none;
padding: 2px 4px;
position: fixed;
bottom: 58px;
left: 20px;
background-color: var(--second-background-color);
z-index: 8;
border-radius: 5px;
max-height: 400px;
}
footer-tag-content{
position: relative;
max-height: 400px;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
overflow-y: scroll;
}
footer-tag-content > * {
margin: 2px 5px;
}
footer-tag-popup::before{
content: "";
width: 0;
height: 0;
position: absolute;
border-right: 10px solid var(--second-background-color);
border-left: 10px solid transparent;
border-top: 10px solid var(--second-background-color);
border-bottom: 10px solid transparent;
left: 0;
bottom: -17px;
border-radius: 0 0 0 5px;
}
#loaderdiv {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
z-index: 200;
}
#loader {
border: 16px solid transparent;
border-top: 16px solid var(--secondary-color);
border-bottom: 16px solid var(--primary-color);
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
position: absolute;
left: calc(50% - 60px);
top: calc(50% - 120px);
z-index: 201;
}
#loaderText {
position: relative;
margin: 0 auto;
top: calc(50% + 80px);
z-index: 201;
text-align: center;
color: var(--second-background-color);
font-size: 20pt;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#jobStatusView {
z-index: 50;
}
#jobStatusView > popup-window {
top: 80px;
width: 50%;
max-height: calc(100% - 140px);
display: flex;
flex-direction: row;
flex-wrap: nowrap;
background-color: transparent;
}
#jobStatusView > popup-window > div {
overflow-y: scroll;
overflow-x: hidden;
width: 50%;
margin: 0;
max-height: 100%;
}
#jobStatusView > popup-window > div > div {
overflow-x: hidden;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
width: 100%;
margin: 0;
}
.jobWrapper {
width: 90%;
margin: 2px 5%;
height: 100px;
position: relative;
flex-shrink: 0;
background-color: rgba(187,187,187,0.4);
border-radius: 3px;
}
.jobWrapper > .jobImage {
height: 90%;
width: 20%;
left: 5px;
object-fit: contain;
position: absolute;
top: 5%;
}
.jobWrapper > .jobTitle {
position: absolute;
left: calc(20% + 10px);
top: 5px;
}
.jobWrapper > .jobProgressBar {
position: absolute;
left: calc(20% + 10px);
bottom: calc(12pt + 10px);
width: calc(80% - 20px);
height: 10px;
}
.jobWrapper > .jobProgressSpan {
position: absolute;
right: 10px;
bottom: calc(12pt + 20px);
width: 60%;
text-align: right;
}
.jobWrapper > .jobCancel {
position: absolute;
right: 10px;
bottom: 5px;
font-size: 12pt;
color: var(--secondary-color);
cursor: pointer;
}

View File

@ -1,709 +0,0 @@
:root{
--background-color: #030304;
--second-background-color: #fff;
--primary-color: #f5a9b8;
--secondary-color: #5bcefa;
--accent-color: #fff;
--topbar-height: 60px;
box-sizing: border-box;
}
body{
padding: 0;
margin: 0;
height: 100vh;
background-color: var(--background-color);
font-family: "Inter", sans-serif;
overflow-x: hidden;
}
wrapper {
display: flex;
flex-flow: column;
flex-wrap: nowrap;
height: 100vh;
}
background-placeholder{
background-color: var(--second-background-color);
opacity: 1;
position: absolute;
width: 100%;
height: 100%;
border-radius: 0 0 5px 0;
z-index: -1;
}
topbar {
display: flex;
align-items: center;
height: var(--topbar-height);
background-color: var(--secondary-color);
z-index: 100;
box-shadow: 0 0 20px black;
}
titlebox {
position: relative;
display: flex;
margin: 0 0 0 40px;
height: 100%;
align-items:center;
justify-content:center;
}
titlebox span{
cursor: default;
font-size: 24pt;
font-weight: bold;
background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-left: 20px;
}
titlebox img {
height: 100%;
margin-right: 10px;
cursor: grab;
}
spacer{
flex-grow: 1;
}
searchdiv{
display: block;
margin: 0 10px 0 0;
}
#searchbox {
padding: 3px 10px;
border: 0;
border-radius: 4px;
font-size: 14pt;
width: 250px;
}
#settingscog {
cursor: pointer;
margin: 0px 30px;
height: 50%;
filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%);
}
viewport {
position: relative;
display: flex;
flex-flow: row;
flex-wrap: nowrap;
flex-grow: 1;
height: 100%;
overflow-y: scroll;
}
footer {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 100%;
height: 40px;
align-items: center;
justify-content: center;
background-color: var(--primary-color);
align-content: center;
}
footer > div {
height: 100%;
margin: 0 30px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
cursor: pointer;
}
footer > div > *{
height: 40%;
margin: 0 5px;
}
#madeWith {
flex-grow: 1;
text-align: right;
margin-right: 20px;
cursor: url("media/blahaj.png"), grab;
}
content {
position: relative;
flex-grow: 1;
border-radius: 5px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
align-content: start;
}
#settingsPopup{
z-index: 300;
}
#settingsPopup popup-content{
flex-direction: column;
align-items: start;
margin: 15px 10px;
}
#settingsPopup popup-content > * {
margin: 5px 10px;
}
#settingsPopup popup-content .title {
font-weight: bolder;
}
#addPublication {
cursor: pointer;
background-color: var(--secondary-color);
width: 180px;
height: 300px;
border-radius: 5px;
margin: 10px 10px;
padding: 15px 20px;
position: relative;
}
#addPublication p{
width: 100%;
text-align: center;
font-size: 150pt;
vertical-align: middle;
line-height: 300px;
margin: 0;
color: var(--accent-color);
}
.pill {
flex-grow: 0;
height: 14pt;
font-size: 12pt;
border-radius: 9pt;
background-color: var(--primary-color);
padding: 2pt 17px;
color: black;
}
publication{
cursor: pointer;
background-color: var(--secondary-color);
width: 180px;
height: 300px;
border-radius: 5px;
margin: 10px 10px;
padding: 15px 20px;
position: relative;
flex-shrink: 0;
}
publication:hover {
background-color: black;
}
publication:hover::after{
background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
}
publication:hover > publication-information {
display: flex;
opacity:1;
}
publication::after{
content: '';
position: absolute;
left: 0; top: 0;
border-radius: 5px;
width: 100%; height: 100%;
}
publication-information {
display: none;
flex-direction: column;
justify-content: start;
}
publication-information * {
z-index: 1;
color: var(--accent-color);
}
connector-name{
width: fit-content;
margin: 10px 0;
}
publication-name{
width: fit-content;
font-size: 16pt;
font-weight: bold;
}
publication-status {
display: inline-block;
height: 10px;
width: 10px;
border-radius: 50%;
margin: 5px;
position: relative;
vertical-align: middle;
}
publication-status:hover > status-tooltip {
visibility: visible;
}
status-tooltip {
visibility: hidden;
position: absolute;
/*Position*/
left: -50px ;
bottom: 150%;
z-index: 3;
/*Text Properties*/
font-size:10pt;
font-weight:bold;
color:white;
text-align: center;
padding: 5px 5px;
/*Size*/
width:100px;
background-color: black;
border-radius: 6px;
}
status-tooltip::after {
content: " ";
position: absolute;
top: 100%; /* At the bottom of the tooltip */
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: black transparent transparent transparent;
}
publication-details {
display: flex;
flex-direction: column;
justify-content: start;
}
publication-details * {
z-index: 1;
color: var(--accent-color);
}
publication img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
border-radius: 5px;
}
popup{
display: none;
width: 100%;
min-height: 100%;
top: 0;
left: 0;
position: fixed;
z-index: 2;
flex-direction: column;
}
popup popup-window {
position: absolute;
z-index: 3;
left: 25%;
top: 100px;
width: 50%;
display: flex;
flex-direction: column;
background-color: var(--second-background-color);
border-radius: 3px;
overflow: hidden;
}
popup popup-window popup-title {
height: 30px;
font-size: 14pt;
font-weight: bolder;
padding: 5px 10px;
margin: 0;
display: flex;
align-items: center;
background-color: var(--primary-color);
color: var(--accent-color)
}
popup popup-window popup-content{
margin: 15px 10px;
display: flex;
align-items: center;
justify-content: space-evenly;
}
popup popup-window popup-content div > * {
margin: 2px 3px 0 0;
}
popup popup-window popup-content input, select {
padding: 3px 4px;
width: 130px;
border: 1px solid lightgrey;
background-color: var(--accent-color);
border-radius: 3px;
}
#newMangaPopup > div {
z-index: 3;
position: relative;
}
#newMangaPopup > #newMangaPopupSelector {
width: 600px;
height: 40px;
margin: 80px auto 0;
}
#newMangaPopup > div > #newMangaConnector, #newMangaTitle, #newMangaTranslatedLanguage {
margin: 0;
display: inline-block;
height: 40px;
}
#newMangaPopup #newMangaConnector {
width: 100px;
padding: 0 0 0 5px;
border-radius: 5px 0 0 5px;
border: 0;
border-right: 1px solid darkgray;
}
#newMangaPopup #newMangaTitle{
width: 445px;
padding: 0 5px 0 5px;
border: 0;
}
#newMangaPopup #newMangaTranslatedLanguage {
width: 45px;
border-radius: 0 5px 5px 0;
border: 0;
border-left: 1px solid darkgray;
margin-left: -5px;
}
#newMangaResult {
display: none;
flex-direction: row;
justify-content: flex-start;
margin: 5px auto 0;
border-radius: 5px;
padding: 5px;
width: min-content;
max-width: 98%;
max-height: 400px;
overflow-x: scroll;
overflow-y: hidden;
}
blur-background {
width: 100%;
height: 100%;
position: absolute;
left: 0;
background: rgba(245, 169, 184, 0.58);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(4.5px);
-webkit-backdrop-filter: blur(4.5px);
}
#publicationViewerPopup{
z-index: 5;
}
publication-viewer{
display: block;
width: 460px;
position: absolute;
top: 200px;
left: 400px;
background-color: var(--accent-color);
border-radius: 5px;
overflow: hidden;
padding: 15px;
}
publication-viewer::after{
content: '';
position: absolute;
left: 0; top: 0;
border-radius: 5px;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
backdrop-filter: blur(3px);
}
publication-viewer img {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
object-fit: cover;
border-radius: 5px;
z-index: 0;
}
publication-viewer publication-details > * {
margin: 5px 0;
}
publication-viewer publication-details publication-name {
width: initial;
overflow-x: scroll;
white-space: nowrap;
scrollbar-width: none;
}
publication-viewer publication-details publication-tags::before {
content: "Tags";
display: block;
font-weight: bolder;
}
publication-viewer publication-details publication-tags {
overflow-x: scroll;
white-space: nowrap;
scrollbar-width: none;
}
publication-viewer publication-details publication-author::before {
content: "Author: ";
font-weight: bolder;
}
publication-viewer publication-details publication-description::before {
content: "Description";
display: block;
font-weight: bolder;
}
publication-viewer publication-details publication-description {
font-size: 12pt;
margin: 5px 0;
height: 145px;
overflow-x: scroll;
}
publication-viewer publication-details publication-interactions {
display: flex;
flex-direction: row;
justify-content: end;
align-items: start;
width: 100%;
}
publication-viewer publication-details publication-interactions > * {
margin: 0 10px;
font-size: 16pt;
cursor: pointer;
}
publication-viewer publication-details publication-interactions publication-starttask {
color: var(--secondary-color);
}
publication-viewer publication-details publication-interactions publication-delete {
color: red;
}
publication-view publication-details publication-interactions publication-canceltask {
color: yellow;
}
publication-viewer publication-details publication-interactions publication-add {
color: limegreen;
}
footer-tag-popup {
display: none;
padding: 2px 4px;
position: fixed;
bottom: 58px;
left: 20px;
background-color: var(--second-background-color);
z-index: 8;
border-radius: 5px;
max-height: 400px;
}
footer-tag-content{
position: relative;
max-height: 400px;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
overflow-y: scroll;
}
footer-tag-content > * {
margin: 2px 5px;
}
footer-tag-popup::before{
content: "";
width: 0;
height: 0;
position: absolute;
border-right: 10px solid var(--second-background-color);
border-left: 10px solid transparent;
border-top: 10px solid var(--second-background-color);
border-bottom: 10px solid transparent;
left: 0;
bottom: -17px;
border-radius: 0 0 0 5px;
}
#loaderdiv {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
z-index: 200;
}
#loader {
border: 16px solid transparent;
border-top: 16px solid var(--secondary-color);
border-bottom: 16px solid var(--primary-color);
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
position: absolute;
left: calc(50% - 60px);
top: calc(50% - 120px);
z-index: 201;
}
#loaderText {
position: relative;
margin: 0 auto;
top: calc(50% + 80px);
z-index: 201;
text-align: center;
color: var(--second-background-color);
font-size: 20pt;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#jobStatusView {
z-index: 50;
}
#jobStatusView > popup-window {
top: 80px;
width: 50%;
max-height: calc(100% - 140px);
display: flex;
flex-direction: row;
flex-wrap: nowrap;
background-color: transparent;
}
#jobStatusView > popup-window > div {
overflow-y: scroll;
overflow-x: hidden;
width: 50%;
margin: 0;
max-height: 100%;
}
#jobStatusView > popup-window > div > div {
overflow-x: hidden;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
width: 100%;
margin: 0;
}
.jobWrapper {
width: 90%;
margin: 2px 5%;
height: 100px;
position: relative;
flex-shrink: 0;
background-color: rgba(187,187,187,0.4);
border-radius: 3px;
}
.jobWrapper > .jobImage {
height: 90%;
width: 20%;
left: 5px;
object-fit: contain;
position: absolute;
top: 5%;
}
.jobWrapper > .jobTitle {
position: absolute;
left: calc(20% + 10px);
top: 5px;
}
.jobWrapper > .jobProgressBar {
position: absolute;
left: calc(20% + 10px);
bottom: calc(12pt + 10px);
width: calc(80% - 20px);
height: 10px;
}
.jobWrapper > .jobProgressSpan {
position: absolute;
right: 10px;
bottom: calc(12pt + 20px);
width: 60%;
text-align: right;
}
.jobWrapper > .jobCancel {
position: absolute;
right: 10px;
bottom: 5px;
font-size: 12pt;
color: var(--secondary-color);
cursor: pointer;
}

View File

@ -1,21 +0,0 @@
version: '3'
services:
tranga-api:
image: glax/tranga-api:cuttingedge
container_name: tranga-api
volumes:
- ./tranga:/usr/share/Tranga-API #1 when replacing ./tranga replace #2 with same value
- ./Manga:/Manga
ports:
- "6531:6531"
restart: unless-stopped
tranga-website:
image: glax/tranga-website:cuttingedge
container_name: tranga-website
volumes:
- ./tranga/imageCache:/usr/share/nginx/html/imageCache:ro #2 when replacing Point to same value as #1/imageCache
ports:
- "9555:80"
depends_on:
- tranga-api
restart: unless-stopped

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

24
tranga-website/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

54
tranga-website/README.md Normal file
View File

@ -0,0 +1,54 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```

View File

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

13
tranga-website/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/blahaj.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tranga</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6394
tranga-website/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
{
"name": "tranga-website",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/inter": "^5.2.5",
"@mui/icons-material": "^7.0.1",
"@mui/joy": "^5.0.0-beta.52",
"@uiw/react-markdown-preview": "^5.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
}
}

View File

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -0,0 +1,13 @@
.app {
position: absolute;
height: 100%;
top: 0;
width: 100%;
left: 0;
}
.app-content {
position: absolute;
width: 100%;
left: 0;
}

107
tranga-website/src/App.tsx Normal file
View File

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

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

@ -0,0 +1,119 @@
import {Badge, Box, Card, CardContent, CardCover, Skeleton, Tooltip, Typography,} from "@mui/joy";
import IManga from "../api/types/IManga.ts";
import {CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
import {GetMangaCoverImageUrl} from "../api/Manga.tsx";
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
import {SxProps} from "@mui/joy/styles/types";
import MangaPopup from "./MangaPopup.tsx";
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx";
import {MangaContext} from "../api/Contexts/MangaContext.tsx";
export const CardWidth = 190;
export const CardHeight = 300;
const coverSx : SxProps = {
height: CardHeight + "px",
width: CardWidth + "px",
position: "relative",
}
const coverCss : CSSProperties = {
maxHeight: "calc("+CardHeight+"px + 2rem)",
maxWidth: "calc("+CardWidth+"px + 2rem)",
objectFit: "cover",
width: "calc("+CardHeight+"px + 2rem)",
height: "calc("+CardHeight+"px + 2rem)",
}
export function MangaFromId({mangaId, children} : { mangaId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
const mangaContext = useContext(MangaContext);
const [manga, setManga] = useState<IManga | undefined>(undefined);
mangaContext.GetManga(mangaId).then(setManga);
return (
manga === undefined ?
<Badge sx={{margin:"8px !important"}} badgeContent={<Skeleton><Tooltip title={"Loading"}><img width={"24pt"} height={"24pt"} src={"/blahaj.png"} /></Tooltip></Skeleton>} color={ReleaseStatusToPalette(MangaReleaseStatus.Completed)} size={"lg"}>
<Card sx={{height:"fit-content",width:"fit-content"}}>
<CardCover>
<img loading={"lazy"} style={coverCss} src={"/blahaj.png"} alt="Manga Cover"/>
</CardCover>
<CardCover sx={{
background:
'linear-gradient(to bottom, rgba(0,0,0,0.4), rgba(0,0,0,0) 200px), linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0) 300px)',
}}/>
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
<Box sx={coverSx}>
<Typography level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
<Skeleton loading={true} animation={"wave"}>
{mangaId.split("").splice(0,mangaId.length/2).join(" ")}
</Skeleton>
</Typography>
</Box>
</CardContent>
</Card>
</Badge>
:
<Manga manga={manga} children={children} />
);
}
export function Manga({manga: manga, children} : { manga: IManga, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
const CoverRef = useRef<HTMLImageElement>(null);
const apiUri = useContext(ApiUriContext);
const mangaConnector = useContext(MangaConnectorContext).find(all => all.name == manga.mangaConnectorName);
const [expanded, setExpanded] = useState(false);
useEffect(() => {
LoadMangaCover();
}, [manga, apiUri]);
const LoadMangaCover = useCallback(() => {
if(CoverRef.current == null)
return;
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
if(CoverRef.current.src == coverUrl)
return;
//Check if we can fetch the image exists (by fetching it), only then update
getData(coverUrl).then(() => {
if(CoverRef.current) CoverRef.current.src = coverUrl;
});
}, [manga, apiUri]);
const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"];
const maxLength = 50;
const mangaName = manga.name.length > maxLength ? manga.name.substring(0, maxLength-3) + "..." : manga.name;
return (
<Badge sx={{margin:"8px !important"}} badgeContent={mangaConnector ? <Tooltip color={ReleaseStatusToPalette(manga.releaseStatus)} title={manga.releaseStatus}><img width={"24pt"} height={"24pt"} src={mangaConnector.iconUrl} /></Tooltip> : manga.mangaConnectorName} color={ReleaseStatusToPalette(manga.releaseStatus)} size={"lg"}>
<Card sx={{height:"fit-content",width:"fit-content"}} onClick={(e) => {
const target = e.target as HTMLElement;
if(interactiveElements.find(x => x == target.localName) == undefined)
setExpanded(!expanded)}
}>
<CardCover>
<img loading={"lazy"} style={coverCss} src={GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current)} alt="Manga Cover"
ref={CoverRef}
onError={LoadMangaCover}/>
</CardCover>
<CardCover sx={{
background:
'linear-gradient(to bottom, rgba(0,0,0,0.4), rgba(0,0,0,0) 200px), linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0) 300px)',
}}/>
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
<Box sx={coverSx}>
<Typography level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
{mangaName}
</Typography>
</Box>
</CardContent>
<MangaPopup manga={manga} open={expanded} setOpen={setExpanded}>{children}</MangaPopup>
</Card>
</Badge>
);
}

View File

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

View File

@ -0,0 +1,186 @@
import IManga from "../api/types/IManga.ts";
import {Badge, Box, Chip, CircularProgress, Drawer, Input, Link, Skeleton, Stack, Typography} from "@mui/joy";
import React, {ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
import {
GetLatestChapterAvailable,
GetLatestChapterDownloaded,
GetMangaCoverImageUrl,
SetIgnoreThreshold
} from "../api/Manga.tsx";
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
import MarkdownPreview from "@uiw/react-markdown-preview";
import {CardHeight} from "./Manga.tsx";
import IChapter from "../api/types/IChapter.ts";
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
import {MangaConnectorContext} from "../api/Contexts/MangaConnectorContext.tsx";
import {MangaContext} from "../api/Contexts/MangaContext.tsx";
import ModalClose from "@mui/joy/ModalClose";
export function MangaPopupFromId({mangaId, open, setOpen, children} : {mangaId: string | null, open: boolean, setOpen: React.Dispatch<React.SetStateAction<boolean>>, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
const mangaContext = useContext(MangaContext);
const [manga, setManga] = useState<IManga | undefined>(undefined);
useEffect(() => {
if (!open || mangaId === null)
return;
mangaContext.GetManga(mangaId).then(setManga);
}, [open]);
return (
manga === undefined ?
<Drawer anchor="bottom" size="lg" open={open} onClose={() => setOpen(false)}>
<ModalClose />
<Stack direction="column" spacing={2} margin={"10px"}>
{ /* Cover and Description */ }
<Stack direction="row" spacing={2} margin={"10px"}>
<Badge sx={{margin:"8px !important"}} color={ReleaseStatusToPalette(MangaReleaseStatus.Unreleased)} size={"lg"}>
<img src="/blahaj.png" alt="Manga Cover"/>
</Badge>
<Box>
<Skeleton loading={true} animation={"wave"}>
{mangaId?.split("").splice(0,mangaId.length/2).join(" ")}
</Skeleton>
<Stack direction={"row"} flexWrap={"wrap"} useFlexGap={true} spacing={0.3} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
{mangaId?.split("").filter(x => Number.isNaN(x)).map(_ =>
<Skeleton loading={true} animation={"wave"}>
<Chip>Wow</Chip>
</Skeleton>
)}
</Stack>
<MarkdownPreview style={{backgroundColor: "transparent", color: "var(--joy-palette-neutral-50)", maxHeight:CardHeight*0.7+"px", overflowY:"auto", marginTop:"10px", scrollbarWidth: "thin"}} />
</Box>
</Stack>
{ /* Actions */ }
<Stack direction="row" spacing={2}>
{children}
</Stack>
</Stack>
</Drawer>
:
<MangaPopup manga={manga} open={open} setOpen={setOpen}>{children}</MangaPopup>
);
}
export default function MangaPopup({manga, open, setOpen, children} : {manga: IManga | null, open: boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
const apiUri = useContext(ApiUriContext);
const CoverRef = useRef<HTMLImageElement>(null);
const LoadMangaCover = useCallback(() => {
if(CoverRef.current == null || manga == null)
return;
if (!open)
return;
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
if(CoverRef.current.src == coverUrl)
return;
//Check if we can fetch the image exists (by fetching it), only then update
getData(coverUrl).then(() => {
if(CoverRef.current) CoverRef.current.src = coverUrl;
});
}, [manga, apiUri, open])
useEffect(() => {
if(!open)
return;
LoadMaxChapter();
LoadDownloadedChapter();
LoadMangaCover();
}, [open]);
const [mangaMaxChapter, setMangaMaxChapter] = useState<IChapter>();
const [maxChapterLoading, setMaxChapterLoading] = useState<boolean>(true);
const LoadMaxChapter = useCallback(() => {
if(manga == null)
return;
setMaxChapterLoading(true);
GetLatestChapterAvailable(apiUri, manga.mangaId)
.then(setMangaMaxChapter)
.finally(() => setMaxChapterLoading(false));
}, [manga, apiUri]);
const [mangaDownloadedChapter, setMangaDownloadedChapter] = useState<IChapter>();
const [downloadedChapterLoading, setDownloadedChapterLoading] = useState<boolean>(true);
const LoadDownloadedChapter = useCallback(() => {
if(manga == null)
return;
setDownloadedChapterLoading(true);
GetLatestChapterDownloaded(apiUri, manga.mangaId)
.then(setMangaDownloadedChapter)
.finally(() => setDownloadedChapterLoading(false));
}, [manga, apiUri]);
const [updatingThreshold, setUpdatingThreshold] = useState<boolean>(false);
const updateIgnoreThreshold = useCallback((value: number) => {
if(manga == null)
return;
setUpdatingThreshold(true);
SetIgnoreThreshold(apiUri, manga.mangaId, value).finally(() => setUpdatingThreshold(false));
},[manga, apiUri])
const mangaConnector = useContext(MangaConnectorContext).find(all => all.name == manga?.mangaConnectorName);
return (
<Drawer anchor="bottom" size="lg" open={open} onClose={() => setOpen(false)}>
<ModalClose />
<Stack direction="column" spacing={2} margin={"10px"}>
{ /* Cover and Description */ }
<Stack direction="row" spacing={2} margin={"10px"}>
<Badge sx={{margin:"8px !important"}} badgeContent={mangaConnector ? <img width={"24pt"} height={"24pt"} src={mangaConnector.iconUrl} /> : manga?.mangaConnectorName} color={ReleaseStatusToPalette(manga?.releaseStatus??MangaReleaseStatus.Unreleased)} size={"lg"}>
<img src="/blahaj.png" alt="Manga Cover"
ref={CoverRef}
onLoad={LoadMangaCover}/>
</Badge>
<Box>
<Link target={"_blank"} href={manga?.websiteUrl} level={"h2"}>
{manga?.name}
</Link>
<Stack direction={"row"} flexWrap={"wrap"} useFlexGap={true} spacing={0.3} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
{manga?.authors?.map(author => <Chip key={author.authorId} variant={"outlined"} size={"md"} color={"success"}>{author.authorName}</Chip>)}
{manga?.mangaTags?.map(tag => <Chip key={tag.tag} variant={"soft"} size={"md"} color={"primary"}>{tag.tag}</Chip>)}
{manga?.links?.map(link =>
<Chip key={link.linkId} variant={"soft"} size={"md"} color={"warning"}>
<Link target={"_blank"} sx={{textDecoration:"underline"}} level={"body-xs"} href={link?.linkUrl}>{link?.linkProvider??"Load Failed"}</Link>
</Chip>
)}
</Stack>
<MarkdownPreview source={manga?.description} style={{backgroundColor: "transparent", color: "var(--joy-palette-neutral-50)", maxHeight:CardHeight*0.7+"px", overflowY:"auto", marginTop:"10px", scrollbarWidth: "thin"}} />
</Box>
</Stack>
{ /* Actions */ }
<Stack direction="row" spacing={2}>
<Input
type={"number"}
placeholder={downloadedChapterLoading ? "" : mangaDownloadedChapter?.chapterNumber??"0.0"}
startDecorator={
<>
{
updatingThreshold ?
<CircularProgress color={"primary"} size={"sm"} />
: <Typography>Ch.</Typography>
}
</>
}
endDecorator={
<Typography>
<Skeleton loading={maxChapterLoading}>
/{mangaMaxChapter?.chapterNumber??"-"}
</Skeleton>
</Typography>
}
sx={{width:"min-content"}}
size={"md"}
onChange={(e) => updateIgnoreThreshold(e.currentTarget.valueAsNumber)}
/>
{children}
</Stack>
</Stack>
</Drawer>
);
}

View File

@ -0,0 +1,202 @@
import {
Avatar,
Button,
Chip,
CircularProgress,
Drawer,
Input,
ListItemDecorator,
Option,
Select, SelectOption,
Skeleton,
Stack,
Step,
StepIndicator,
Stepper,
Typography
} from "@mui/joy";
import ModalClose from "@mui/joy/ModalClose";
import IMangaConnector from "../api/types/IMangaConnector";
import {useCallback, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx";
import {GetAllConnectors} from "../api/MangaConnector.tsx";
import IManga from "../api/types/IManga.ts";
import {SearchNameOnConnector, SearchUrl} from "../api/Search.tsx";
import {Manga} from "./Manga.tsx";
import Add from "@mui/icons-material/Add";
import React from "react";
import {CreateDownloadAvailableChaptersJob} from "../api/Job.tsx";
import ILocalLibrary from "../api/types/ILocalLibrary.ts";
import {GetLibraries} from "../api/LocalLibrary.tsx";
import { LibraryBooks } from "@mui/icons-material";
export default function Search({open, setOpen}:{open:boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>}){
const [step, setStep] = useState<number>(2);
const apiUri = useContext(ApiUriContext);
const [mangaConnectors, setMangaConnectors] = useState<IMangaConnector[]>();
const [mangaConnectorsLoading, setMangaConnectorsLoading] = useState<boolean>(true);
const [selectedMangaConnector, setSelectedMangaConnector] = useState<IMangaConnector>();
const loadMangaConnectors = useCallback(() => {
setMangaConnectorsLoading(true);
GetAllConnectors(apiUri).then(setMangaConnectors).finally(() => setMangaConnectorsLoading(false));
}, [apiUri]);
const [results, setResults] = useState<IManga[]|undefined>([]);
const [resultsLoading, setResultsLoading] = useState<boolean>(false);
const StartSearch = useCallback((mangaConnector : IMangaConnector | undefined, value: string)=>{
if(mangaConnector === undefined && !IsValidUrl(value))
return;
setResults(undefined);
setResultsLoading(true);
setStep(3);
if (IsValidUrl(value)){
SearchUrl(apiUri, value).then((r) => setResults([r])).finally(() => setResultsLoading(false));
}else if (mangaConnector != undefined){
SearchNameOnConnector(apiUri, mangaConnector.name, value).then(setResults).finally(() => setResultsLoading(false));
}
},[apiUri])
function IsValidUrl(str : string) : boolean {
const pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
'(\\#[-a-z\\d_]*)?$','i'); // fragment locator
return !!pattern.test(str);
}
const [localLibraries, setLocalLibraries] = useState<ILocalLibrary[]>();
const [localLibrariesLoading, setLocalLibrariesLoading] = useState<boolean>(true);
const [selectedLibraryId, setSelectedLibraryId] = useState<string>();
const loadLocalLibraries = useCallback(() => {
setLocalLibrariesLoading(true);
GetLibraries(apiUri).then(setLocalLibraries).finally(() => setLocalLibrariesLoading(false));
}, [apiUri]);
useEffect(() => {
loadMangaConnectors();
loadLocalLibraries();
},[apiUri]);
useEffect(() => {
loadMangaConnectors();
loadLocalLibraries();
}, []);
function renderValue(option: SelectOption<string> | null) {
if (!option) {
return null;
}
return (
<React.Fragment>
<ListItemDecorator>
<Avatar size="sm" src={mangaConnectors?.find((o) => o.name === option.value)?.iconUrl} />
</ListItemDecorator>
{option.label}
</React.Fragment>
);
}
// @ts-ignore
return (
<Drawer size={"lg"} anchor={"right"} open={open} onClose={() => {
if(step > 2)
setStep(2);
setResults([]);
setOpen(false);
}}>
<ModalClose />
<Stepper orientation={"vertical"} sx={{ height: '100%', width: "calc(100% - 80px)", margin:"40px"}}>
<Step indicator={
<StepIndicator variant={step==1?"solid":"outlined"} color={(mangaConnectors?.length??0) < 1 ? "danger" : "primary"}>
1
</StepIndicator>}>
<Skeleton loading={mangaConnectorsLoading}>
<Select
color={(mangaConnectors?.length??0) < 1 ? "danger" : "neutral"}
disabled={mangaConnectorsLoading || resultsLoading || mangaConnectors?.length == null || mangaConnectors.length < 1}
placeholder={"Select Connector"}
slotProps={{
listbox: {
sx: {
'--ListItemDecorator-size': '44px',
},
},
}}
sx={{ '--ListItemDecorator-size': '44px', minWidth: 240 }}
renderValue={renderValue}
onChange={(_e, newValue) => {
setSelectedMangaConnector(mangaConnectors?.find((o) => o.name === newValue));
setStep(2);
setResults(undefined);
}}
endDecorator={<Chip size={"sm"} color={mangaConnectors?.length??0 < 1 ? "danger" : "primary"}>{mangaConnectors?.length}</Chip>}>
{mangaConnectors?.map((connector: IMangaConnector) => ConnectorOption(connector))}
</Select>
</Skeleton>
</Step>
<Step indicator={
<StepIndicator variant={step==2?"solid":"outlined"} color="primary">
2
</StepIndicator>}>
<Input disabled={resultsLoading} placeholder={"Name or Url " + (selectedMangaConnector ? selectedMangaConnector.baseUris[0] : "")} onKeyDown={(e) => {
setStep(2);
setResults(undefined);
if(e.key === "Enter") {
StartSearch(selectedMangaConnector, e.currentTarget.value);
}
}}/>
</Step>
<Step indicator={
<StepIndicator variant={step==3?"solid":"outlined"} color="primary">
3
</StepIndicator>}>
<Typography endDecorator={<Chip size={"sm"} color={"primary"}>{results?.length??"-"}</Chip>}>Results</Typography>
<Skeleton loading={resultsLoading}>
<Stack direction={"row"} spacing={1} flexWrap={"wrap"}>
{results?.map((result) =>
<Manga key={result.mangaId} manga={result}>
<Select
placeholder={"Select Library"}
defaultValue={""}
startDecorator={<LibraryBooks />}
value={selectedLibraryId}
onChange={(_e, newValue) => setSelectedLibraryId(newValue!)}>
{localLibrariesLoading ?
<Option value={""} disabled>Loading <CircularProgress color={"primary"} size={"sm"} /></Option>
:
(localLibraries??[]).map(library => {
return (
<Option value={library.localLibraryId}>{library.libraryName} ({library.basePath})</Option>
);
})}
</Select>
<Button disabled={localLibrariesLoading || selectedLibraryId === undefined} onClick={() => {
CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {localLibraryId: selectedLibraryId!,recurrenceTimeMs: 1000 * 60 * 60 * 3, language: "en"})
}} endDecorator={<Add />}>Watch</Button>
</Manga>)}
</Stack>
</Skeleton>
</Step>
</Stepper>
</Drawer>
);
}
function ConnectorOption(connector: IMangaConnector){
return (
<Option key={connector.name} value={connector.name} sx={{position: "relative"}}>
<ListItemDecorator>
<Avatar size="sm" src={connector.iconUrl} />
</ListItemDecorator>
{connector.name}
</Option>
);
}

View File

@ -0,0 +1,59 @@
import IBackendSettings from "../../api/types/IBackendSettings.ts";
import {useCallback, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx";
import {
Accordion,
AccordionDetails,
AccordionSummary,
ColorPaletteProp,
Switch,
Typography
} from "@mui/joy";
import * as React from "react";
import {GetAprilFoolsToggle, UpdateAprilFoolsToggle} from "../../api/BackendSettings.tsx";
export default function ImageProcessing({backendSettings}: {backendSettings?: IBackendSettings}) {
const apiUri = useContext(ApiUriContext);
const [loading, setLoading] = useState<boolean>(false);
const [color, setColor] = useState<ColorPaletteProp>("neutral");
const [value, setValue] = useState<boolean>(backendSettings?.aprilFoolsMode??false);
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
const valueChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
setColor("warning");
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
UpdateAprilFoolsMode(e.target.checked);
}, 1000);
}
useEffect(() => {
setValue(backendSettings?.aprilFoolsMode??false);
}, [backendSettings]);
const UpdateAprilFoolsMode = useCallback((val: boolean) => {
UpdateAprilFoolsToggle(apiUri, val)
.then(() => GetAprilFoolsToggle(apiUri))
.then((val) => setValue(val))
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}, [apiUri]);
return (
<Accordion>
<AccordionSummary>April Fools Mode</AccordionSummary>
<AccordionDetails>
<Typography endDecorator={
<Switch disabled={backendSettings === undefined || loading}
onChange={valueChanged}
color={color}
checked={value} />
}>
Toggle
</Typography>
</AccordionDetails>
</Accordion>
);
}

View File

@ -0,0 +1,74 @@
import IBackendSettings from "../../api/types/IBackendSettings";
import {
Accordion,
AccordionDetails,
AccordionSummary, Chip,
CircularProgress,
ColorPaletteProp,
Divider,
Input,
Stack, Tooltip, Typography
} from "@mui/joy";
import {KeyboardEventHandler, useCallback, useContext, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx";
import {UpdateChapterNamingScheme} from "../../api/BackendSettings.tsx";
export default function ChapterNamingScheme({backendSettings}: {backendSettings?: IBackendSettings}) {
const apiUri = useContext(ApiUriContext);
const [loading, setLoading] = useState<boolean>(false);
const [value, setValue] = useState<string>("");
const [color, setColor] = useState<ColorPaletteProp>("neutral");
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
if(e.key === "Enter") {
setLoading(true);
UpdateChapterNamingScheme(apiUri, value)
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}
}, [apiUri])
return (
<Accordion>
<AccordionSummary>Chapter Naming Scheme</AccordionSummary>
<AccordionDetails>
<Input disabled={backendSettings === undefined || loading}
placeholder={"Chapter Naming Scheme"}
defaultValue={backendSettings?.chapterNamingScheme}
onKeyDown={keyDown}
onChange={e => setValue(e.target.value)}
color={color}
endDecorator={(loading ? <CircularProgress color={"primary"} size={"sm"} /> : null)}
/>
<Typography level={"title-sm"}>Placeholders:</Typography>
<Stack direction="row" spacing={1} divider={<Divider />}>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"Manga Title"} >
<Chip color={"primary"}>%M</Chip>
</Tooltip>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"Volume Number"} >
<Chip color={"primary"}>%V</Chip>
</Tooltip>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"Chapter Number"} >
<Chip color={"primary"}>%C</Chip>
</Tooltip>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"Chapter Title"} >
<Chip color={"primary"}>%T</Chip>
</Tooltip>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"Year"} >
<Chip color={"primary"}>%Y</Chip>
</Tooltip>
<Tooltip arrow placement="bottom" size="md" variant="outlined"
title={"First Author"} >
<Chip color={"primary"}>%A</Chip>
</Tooltip>
</Stack>
</AccordionDetails>
</Accordion>
);
}

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

@ -0,0 +1,102 @@
import IBackendSettings from "../../api/types/IBackendSettings.ts";
import {ChangeEvent, useCallback, useContext, useEffect, useRef, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx";
import {
Accordion,
AccordionDetails,
AccordionSummary, ColorPaletteProp, Input, Stack, Switch, Typography,
} from "@mui/joy";
import {
GetBWImageToggle,
GetImageCompressionValue,
UpdateBWImageToggle,
UpdateImageCompressionValue
} from "../../api/BackendSettings.tsx";
export default function ImageProcessing ({backendSettings}: { backendSettings?: IBackendSettings }) {
const apiUri = useContext(ApiUriContext);
useEffect(() => {
setBwImages(backendSettings?.bwImages??false);
setCompression(backendSettings?.compression??100);
}, [backendSettings]);
const [bwImages, setBwImages] = useState<boolean>(backendSettings?.bwImages??false);
const [bwImagesLoading, setBwImagesLoading] = useState(false);
const [bwImagesColor, setBwImagesColor] = useState<ColorPaletteProp>("neutral");
const bwTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const bwValueChanged = (e : ChangeEvent<HTMLInputElement>) => {
setBwImages(e.target.checked);
setBwImagesColor("warning");
clearTimeout(bwTimerRef.current);
bwTimerRef.current = setTimeout(() => {
UpdateBwImages(e.target.checked);
}, 1000);
}
const UpdateBwImages = useCallback((val : boolean) => {
setBwImagesLoading(true);
UpdateBWImageToggle(apiUri, val)
.then(() => GetBWImageToggle(apiUri))
.then(setBwImages)
.then(() => setBwImagesColor("success"))
.catch(() => setBwImagesColor("danger"))
.finally(() => setBwImagesLoading(false));
},[apiUri]);
const [compression, setCompression] = useState<number>(backendSettings?.compression??100);
const [compressionLoading, setCompressionLoading] = useState(false);
const [compressionColor, setCompressionColor] = useState<ColorPaletteProp>("neutral");
const compressionTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const compressionCheckedChanged = (e : ChangeEvent<HTMLInputElement>) => {
setCompressionColor("warning");
if(!e.target.checked)
setCompression(100);
else
setCompression(50);
clearTimeout(compressionTimerRef.current);
bwTimerRef.current = setTimeout(() => {
UpdateImageCompression(e.target.checked ? 50 : 100);
}, 1000);
}
const compressionValueChanged = (e : ChangeEvent<HTMLInputElement>) => {
setCompressionColor("warning");
setCompression(parseInt(e.target.value));
clearTimeout(compressionTimerRef.current);
bwTimerRef.current = setTimeout(() => {
UpdateImageCompression(parseInt(e.target.value));
}, 1000);
}
const UpdateImageCompression = useCallback((val : number) => {
setCompressionLoading(true);
UpdateImageCompressionValue(apiUri, val)
.then(() => GetImageCompressionValue(apiUri))
.then(setCompression)
.then(() => setCompressionColor("success"))
.catch(() => setCompressionColor("danger"))
.finally(() => setCompressionLoading(false));
},[apiUri]);
return (
<Accordion>
<AccordionSummary>Image Processing</AccordionSummary>
<AccordionDetails>
<Stack>
<Typography endDecorator={
<Switch disabled={backendSettings === undefined || bwImagesLoading}
onChange={bwValueChanged}
color={bwImagesColor}
checked={bwImages} />
}>B/W Images</Typography>
<Typography endDecorator={
<Input type={"number"} value={compression} onChange={compressionValueChanged} startDecorator={
<Switch disabled={backendSettings === undefined || compressionLoading}
onChange={compressionCheckedChanged}
color={compressionColor}
checked={compression < 100} />
} />
}>Compression</Typography>
</Stack>
</AccordionDetails>
</Accordion>
);
}

View File

@ -0,0 +1,81 @@
import IBackendSettings from "../../api/types/IBackendSettings.ts";
import {useCallback, useContext, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Button,
ColorPaletteProp,
Input,
Stack,
Typography
} from "@mui/joy";
import {RequestLimitType} from "../../api/types/EnumRequestLimitType.ts";
import {ResetRequestLimit, ResetRequestLimits, UpdateRequestLimit} from "../../api/BackendSettings.tsx";
import {Restore} from "@mui/icons-material";
export default function RequestLimits({backendSettings}: {backendSettings?: IBackendSettings}) {
const apiUri = useContext(ApiUriContext);
const [color, setColor] = useState<ColorPaletteProp>("neutral");
const [loading, setLoading] = useState(false);
const Update = useCallback((target: HTMLInputElement, limit: RequestLimitType) => {
setLoading(true);
UpdateRequestLimit(apiUri, limit, Number.parseInt(target.value))
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
},[apiUri])
const Reset = useCallback((limit: RequestLimitType) => {
setLoading(true);
ResetRequestLimit(apiUri, limit)
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}, [apiUri]);
const ResetAll = useCallback(() => {
setLoading(true);
ResetRequestLimits(apiUri)
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}, [apiUri]);
return (
<Accordion>
<AccordionSummary>Request Limits</AccordionSummary>
<AccordionDetails>
<Stack spacing={1} direction="column">
<Button loading={backendSettings === undefined} onClick={ResetAll} size={"sm"} variant={"outlined"} endDecorator={<Restore />} color={"warning"}>Reset all</Button>
<Item type={RequestLimitType.Default} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
<Item type={RequestLimitType.MangaInfo} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
<Item type={RequestLimitType.MangaImage} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
<Item type={RequestLimitType.MangaDexFeed} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
<Item type={RequestLimitType.MangaDexImage} color={color} backendSettings={backendSettings} loading={loading} Reset={Reset} Update={Update} />
</Stack>
</AccordionDetails>
</Accordion>
);
}
function Item({type, color, loading, backendSettings, Reset, Update}:
{type: RequestLimitType, color: ColorPaletteProp, loading: boolean, backendSettings: IBackendSettings | undefined, Reset: (x: RequestLimitType) => void, Update: (a: HTMLInputElement, x: RequestLimitType) => void}) {
return (
<Input slotProps={{input: {min: 0, max: 360}}}
color={color}
startDecorator={<Typography sx={{width:"140px"}}>{type}</Typography>}
endDecorator={<Button onClick={() => Reset(type)}>Reset</Button>}
disabled={loading} type={"number"}
defaultValue={backendSettings?.requestLimits[type]}
placeholder={"Default"}
required
onKeyDown={(e) => {
if(e.key == "Enter")
Update(e.target as HTMLInputElement, type);
}}
/>
);
}

View File

@ -0,0 +1,60 @@
import IBackendSettings from "../../api/types/IBackendSettings";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Button,
ColorPaletteProp,
Input
} from "@mui/joy";
import {KeyboardEventHandler, useCallback, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../../api/fetchApi.tsx";
import {GetUserAgent, ResetUserAgent, UpdateUserAgent} from "../../api/BackendSettings.tsx";
export default function UserAgent({backendSettings}: {backendSettings?: IBackendSettings}) {
const apiUri = useContext(ApiUriContext);
const [loading, setLoading] = useState<boolean>(false);
const [value, setValue] = useState<string>(backendSettings?.userAgent??"");
const [color, setColor] = useState<ColorPaletteProp>("neutral");
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
if(value === undefined) return;
if(e.key === "Enter") {
setLoading(true);
UpdateUserAgent(apiUri, value)
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}
}, [apiUri, value])
const Reset = useCallback(() => {
setLoading(true);
ResetUserAgent(apiUri)
.then(() => GetUserAgent(apiUri))
.then((val) => setValue(val))
.then(() => setColor("success"))
.catch(() => setColor("danger"))
.finally(() => setLoading(false));
}, [apiUri]);
useEffect(() => {
setValue(backendSettings?.userAgent??"");
}, [backendSettings]);
return (
<Accordion>
<AccordionSummary>UserAgent</AccordionSummary>
<AccordionDetails>
<Input disabled={backendSettings === undefined || loading}
placeholder={"UserAgent"}
value={value}
onKeyDown={keyDown}
onChange={e => setValue(e.target.value)}
color={color}
endDecorator={<Button onClick={Reset} loading={loading}>Reset</Button>}
/>
</AccordionDetails>
</Accordion>
);
}

View File

@ -0,0 +1,11 @@
.header {
position: sticky !important;
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 60px;
padding: 10px;
display: flex;
flex-flow: row nowrap;
}

View File

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

View File

View File

@ -0,0 +1,127 @@
import Drawer from '@mui/joy/Drawer';
import ModalClose from '@mui/joy/ModalClose';
import {
Accordion,
AccordionDetails,
AccordionGroup,
AccordionSummary, CircularProgress, ColorPaletteProp,
DialogContent,
DialogTitle, Input,
Link, Stack
} from "@mui/joy";
import './Settings.css';
import * as React from "react";
import {useCallback, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "./api/fetchApi.tsx";
import IBackendSettings from "./api/types/IBackendSettings.ts";
import { GetSettings } from './api/BackendSettings.tsx';
import UserAgent from "./Components/Settings/UserAgent.tsx";
import ImageProcessing from "./Components/Settings/ImageProcessing.tsx";
import ChapterNamingScheme from "./Components/Settings/ChapterNamingScheme.tsx";
import AprilFoolsMode from './Components/Settings/AprilFoolsMode.tsx';
import RequestLimits from "./Components/Settings/RequestLimits.tsx";
import FlareSolverr from "./Components/Settings/FlareSolverr.tsx";
import {Article} from '@mui/icons-material';
const checkConnection = async (apiUri: string): Promise<boolean> =>{
return fetch(`${apiUri}/swagger/v2/swagger.json`,
{
method: 'GET',
})
.then((response) => {
if(!(response.ok && response.status == 200))
return false;
return response.json().then((json) => (json as {openapi:string}).openapi.match("[0-9]+(?:\.[0-9]+)+")?true:false).catch(() => false);
})
.catch(() => {
return false;
});
}
export default function Settings({open, setOpen, setApiUri, setConnected}:{open:boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>, setApiUri:React.Dispatch<React.SetStateAction<string>>, setConnected:React.Dispatch<React.SetStateAction<boolean>>}) {
const apiUri = useContext(ApiUriContext);
const [apiUriColor, setApiUriColor] = useState<ColorPaletteProp>("neutral");
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
const [apiUriAccordionOpen, setApiUriAccordionOpen] = React.useState(true);
const [checking, setChecking] = useState(false);
useEffect(() => {
OnCheckConnection(apiUri);
}, []);
const apiUriChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
clearTimeout(timerRef.current);
setApiUriColor("warning");
timerRef.current = setTimeout(() => {
OnCheckConnection(e.target.value);
}, 1000);
}
const OnCheckConnection = (uri: string) => {
console.log("Checking connection...");
setChecking(true);
checkConnection(uri)
.then((result) => {
setConnected(result);
if(result)
console.log("Connected!");
setApiUriAccordionOpen(!result);
setApiUriColor(result ? "success" : "danger");
if(result)
setApiUri(uri);
})
.finally(() => setChecking(false));
}
const [backendSettings, setBackendSettings] = useState<IBackendSettings>();
const getBackendSettings = useCallback(() => {
GetSettings(apiUri).then(setBackendSettings);
}, [apiUri]);
useEffect(() => {
getBackendSettings();
}, [checking]);
return (
<Drawer size={"lg"} open={open} onClose={() => setOpen(false)}>
<ModalClose />
<DialogTitle>Settings</DialogTitle>
<DialogContent>
<AccordionGroup>
<Accordion expanded={apiUriAccordionOpen} onChange={(_e, expanded) => setApiUriAccordionOpen(expanded)}>
<AccordionSummary>ApiUri</AccordionSummary>
<AccordionDetails>
<Input
disabled={checking}
color={apiUriColor}
placeholder={"http(s)://"}
type={"url"}
defaultValue={apiUri}
onChange={apiUriChanged}
onKeyDown={(e) => {
if(e.key === "Enter") {
clearTimeout(timerRef.current);
OnCheckConnection(e.currentTarget.value);
}
}}
endDecorator={(checking ? <CircularProgress color={apiUriColor} size={"sm"} /> : null)} />
</AccordionDetails>
</Accordion>
<UserAgent backendSettings={backendSettings} />
<ImageProcessing backendSettings={backendSettings} />
<ChapterNamingScheme backendSettings={backendSettings} />
<AprilFoolsMode backendSettings={backendSettings} />
<RequestLimits backendSettings={backendSettings} />
<FlareSolverr backendSettings={backendSettings} />
</AccordionGroup>
<Stack spacing={2} direction="row">
<Link target={"_blank"} href={apiUri + "/swagger"}><Article />Swagger Doc</Link>
</Stack>
</DialogContent>
</Drawer>
);
}

View File

@ -0,0 +1,92 @@
import {deleteData, getData, patchData, postData} from './fetchApi.tsx';
import IBackendSettings from "./types/IBackendSettings.ts";
import IRequestLimits from "./types/IRequestLimits.ts";
import {RequestLimitType} from "./types/EnumRequestLimitType.ts";
export const GetSettings = async (apiUri: string) : Promise<IBackendSettings> => {
return await getData(`${apiUri}/v2/Settings`) as Promise<IBackendSettings>;
}
export const GetUserAgent = async (apiUri: string) : Promise<string> => {
return await getData(`${apiUri}/v2/Settings/UserAgent`) as Promise<string>;
}
export const UpdateUserAgent = async (apiUri: string, userAgent: string)=> {
if(userAgent === undefined || userAgent === null)
return Promise.reject(`userAgent was not provided`);
return patchData(`${apiUri}/v2/Settings/UserAgent`, userAgent);
}
export const ResetUserAgent = async (apiUri: string) => {
return deleteData(`${apiUri}/v2/Settings/UserAgent`);
}
export const GetRequestLimits = async(apiUri: string) : Promise<IRequestLimits> => {
return await getData(`${apiUri}/v2/Settings/RequestLimits`) as Promise<IRequestLimits>;
}
export const ResetRequestLimits = async (apiUri: string) => {
return deleteData(`${apiUri}/v2/Settings/RequestLimits`);
}
export const UpdateRequestLimit = async (apiUri: string, requestType: RequestLimitType, value: number) => {
if(requestType === undefined || requestType === null || value === undefined || value === null)
return Promise.reject();
return patchData(`${apiUri}/v2/Settings/RequestLimits/${requestType}`, value);
}
export const ResetRequestLimit = async (apiUri: string, requestType: RequestLimitType) => {
if(requestType === undefined || requestType === null)
return Promise.reject("requestType was not provided");
return deleteData(`${apiUri}/v2/Settings/RequestLimits/${requestType}`);
}
export const GetImageCompressionValue = async (apiUri: string) : Promise<number> => {
return await getData(`${apiUri}/v2/Settings/ImageCompression`) as Promise<number>;
}
export const UpdateImageCompressionValue = async (apiUri: string, value: number) => {
if(value === undefined || value === null)
return Promise.reject("value was not provided");
return patchData(`${apiUri}/v2/Settings/ImageCompression`, value);
}
export const GetBWImageToggle = async (apiUri: string) : Promise<boolean> => {
return await getData(`${apiUri}/v2/Settings/BWImages`) as Promise<boolean>;
}
export const UpdateBWImageToggle = async (apiUri: string, value: boolean) => {
if(value === undefined || value === null)
return Promise.reject("value was not provided");
return patchData(`${apiUri}/v2/Settings/BWImages`, value);
}
export const GetAprilFoolsToggle = async (apiUri: string) : Promise<boolean> => {
return await getData(`${apiUri}/v2/Settings/AprilFoolsMode`) as Promise<boolean>;
}
export const UpdateAprilFoolsToggle = async (apiUri: string, value: boolean) => {
if(value === undefined || value === null)
return Promise.reject("value was not provided");
return patchData(`${apiUri}/v2/Settings/AprilFoolsMode`, value);
}
export const GetChapterNamingScheme = async (apiUri: string) : Promise<string> => {
return await getData(`${apiUri}/v2/Settings/ChapterNamingScheme`) as Promise<string>;
}
export const UpdateChapterNamingScheme = async (apiUri: string, value: string) => {
return patchData(`${apiUri}/v2/Settings/ChapterNamingScheme`, value);
}
export const SetFlareSolverrUrl = async (apiUri: string, value: string) => {
return postData(`${apiUri}/v2/Settings/FlareSolverr/Url`, value);
}
export const ResetFlareSolverrUrl = async (apiUri: string) => {
return deleteData(`${apiUri}/v2/Settings/FlareSolverr/Url`);
}
export const TestFlareSolverrUrl = async (apiUri: string) => {
return postData(`${apiUri}/v2/Settings/FlareSolverr/Test`);
}

View File

@ -0,0 +1,8 @@
import {getData} from "./fetchApi.tsx";
import IChapter from "./types/IChapter.ts";
export const GetChapterFromId = async (apiUri: string, chapterId: string): Promise<IChapter> => {
if(chapterId === undefined || chapterId === null)
return Promise.reject(`chapterId was not provided`);
return await getData(`${apiUri}/v2/Query/Chapter/${chapterId}`) as Promise<IChapter>;
}

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,4 @@
import {createContext} from "react";
import IMangaConnector from "../types/IMangaConnector.ts";
export const MangaConnectorContext = createContext<IMangaConnector[]>([]);

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

@ -0,0 +1,97 @@
import {deleteData, getData, patchData, postData, putData} from "./fetchApi";
import IJob, {JobState, JobType} from "./types/Jobs/IJob";
import IModifyJobRecord from "./types/records/IModifyJobRecord";
import IDownloadAvailableChaptersJobRecord from "./types/records/IDownloadAvailableChaptersJobRecord.ts";
export const GetAllJobs = async (apiUri: string) : Promise<IJob[]> => {
return await getData(`${apiUri}/v2/Job`) as Promise<IJob[]>;
}
export const GetJobsWithIds = async (apiUri: string, jobIds: string[]) : Promise<IJob[]> => {
if(jobIds === null || jobIds === undefined || jobIds.length === 0)
return Promise.reject("jobIds was not provided");
return await postData(`${apiUri}/v2/Job/WithIDs`, jobIds) as Promise<IJob[]>;
}
export const GetJobsInState = async (apiUri: string, state: JobState) : Promise<IJob[]> => {
if(state == null)
return Promise.reject("state was not provided");
return await getData(`${apiUri}/v2/Job/State/${state}`) as Promise<IJob[]>;
}
export const GetJobsWithType = async (apiUri: string, jobType: JobType) : Promise<IJob[]> => {
if(jobType == null) {
return Promise.reject("jobType was not provided");
}
return await getData(`${apiUri}/v2/Job/Type/${jobType}`) as Promise<IJob[]>;
}
export const GetJobsOfTypeAndWithState = async (apiUri: string, jobType: JobType, state: JobState) : Promise<IJob[]> => {
if(jobType == null)
return Promise.reject("jobType was not provided");
if(state == null)
return Promise.reject("state was not provided");
return await getData(`${apiUri}/v2/Job/TypeAndState/${jobType}/${state}`) as Promise<IJob[]>;
}
export const GetJob = async (apiUri: string, jobId: string) : Promise<IJob> => {
if(jobId === undefined || jobId === null || jobId.length < 1)
return Promise.reject("jobId was not provided");
return await getData(`${apiUri}/v2/Job/${jobId}`) as Promise<IJob>;
}
export const DeleteJob = async (apiUri: string, jobId: string) : Promise<void> => {
if(jobId === undefined || jobId === null || jobId.length < 1)
return Promise.reject("jobId was not provided");
return await deleteData(`${apiUri}/v2/Job/${jobId}`);
}
export const ModifyJob = async (apiUri: string, jobId: string, modifyData: IModifyJobRecord) : Promise<IJob> => {
if(jobId === undefined || jobId === null || jobId.length < 1)
return Promise.reject("jobId was not provided");
if(modifyData === undefined || modifyData === null)
return Promise.reject("modifyData was not provided");
return await patchData(`${apiUri}/v2/Job/${jobId}`, modifyData) as Promise<IJob>;
}
export const CreateDownloadAvailableChaptersJob = async (apiUri: string, mangaId: string, data: IDownloadAvailableChaptersJobRecord) : Promise<string[]> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
if(data === undefined || data === null)
return Promise.reject("data was not provided");
return await putData(`${apiUri}/v2/Job/DownloadAvailableChaptersJob/${mangaId}`, data) as Promise<string[]>;
}
export const CreateDownloadSingleChapterJob = async (apiUri: string, chapterId: string) : Promise<string[]> => {
if(chapterId === undefined || chapterId === null || chapterId.length < 1)
return Promise.reject("chapterId was not provided");
return await putData(`${apiUri}/v2/Job/DownloadSingleChapterJob/${chapterId}`, {}) as Promise<string[]>;
}
export const CreateUpdateFilesJob = async (apiUri: string, mangaId: string) : Promise<string[]> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
return await putData(`${apiUri}/v2/Job/UpdateFilesJob/${mangaId}`, {}) as Promise<string[]>;
}
export const CreateUpdateAllFilesJob = async (apiUri: string) : Promise<string[]> => {
return await putData(`${apiUri}/v2/Job/UpdateAllFilesJob`, {}) as Promise<string[]>;
}
export const CreateUpdateMetadataJob = async (apiUri: string, mangaId: string) : Promise<string[]> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
return await putData(`${apiUri}/v2/Job/UpdateMetadataJob/${mangaId}`, {}) as Promise<string[]>;
}
export const CreateUpdateAllMetadataJob = async (apiUri: string) : Promise<string[]> => {
return await putData(`${apiUri}/v2/Job/UpdateAllMetadataJob`, {}) as Promise<string[]>;
}
export const StartJob = async (apiUri: string, jobId: string, startDependencies: boolean) : Promise<object | undefined> => {
return await postData(`${apiUri}/v2/Job/${jobId}/Start`, startDependencies);
}
export const StopJob = async (apiUri: string, jobId: string) : Promise<object | undefined> => {
return await postData(`${apiUri}/v2/Job/${jobId}/Stop`);
}

View File

@ -0,0 +1,31 @@
import ILocalLibrary from "./types/ILocalLibrary.ts";
import {deleteData, getData, patchData, putData} from "./fetchApi.tsx";
import INewLibraryRecord from "./types/records/INewLibraryRecord.ts";
export const GetLibraries = async (apiUri: string) : Promise<ILocalLibrary[]> => {
return await getData(`${apiUri}/v2/LocalLibraries`) as Promise<ILocalLibrary[]>;
}
export const GetLibrary = async (apiUri: string, libraryId: string) : Promise<ILocalLibrary> => {
return await getData(`${apiUri}/v2/LocalLibraries/${libraryId}`) as Promise<ILocalLibrary>;
}
export const CreateLibrary = async (apiUri: string, data: INewLibraryRecord) : Promise<ILocalLibrary> => {
return await putData(`${apiUri}/v2/LocalLibraries`, data) as Promise<ILocalLibrary>
}
export const DeleteLibrary = async (apiUri: string, libraryId: string) : Promise<void> => {
return await deleteData(`${apiUri}/v2/LocalLibraries/${libraryId}`);
}
export const ChangeLibraryPath = async (apiUri: string, libraryId: string, newPath: string) : Promise<object | undefined> => {
return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}/ChangeBasePath`, newPath);
}
export const ChangeLibraryName = async (apiUri: string, libraryId: string, newName: string) : Promise<object | undefined> => {
return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}/ChangeName`, newName);
}
export const UpdateLibrary = async (apiUri: string, libraryId: string, record: INewLibraryRecord) : Promise<object | undefined> => {
return await patchData(`${apiUri}/v2/LocalLibraries/${libraryId}`, record);
}

View File

@ -0,0 +1,95 @@
import {deleteData, getData, patchData, postData} from './fetchApi.tsx';
import IManga, {DefaultManga} from "./types/IManga.ts";
import IChapter from "./types/IChapter.ts";
export const GetAllManga = async (apiUri: string) : Promise<IManga[]> => {
return await getData(`${apiUri}/v2/Manga`) as Promise<IManga[]>;
}
export const GetMangaWithIds = async (apiUri: string, mangaIds: string[]) : Promise<IManga[]> => {
if(mangaIds === undefined || mangaIds === null || mangaIds.length < 1)
return Promise.reject("mangaIds was not provided");
return await postData(`${apiUri}/v2/Manga/WithIds`, mangaIds) as Promise<IManga[]>;
}
export const GetMangaById = async (apiUri: string, mangaId: string) : Promise<IManga> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}`) as Promise<IManga>;
}
export const DeleteManga = async (apiUri: string, mangaId: string) : Promise<void> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await deleteData(`${apiUri}/v2/Manga/${mangaId}`);
}
export const GetMangaCoverImageUrl = (apiUri: string, mangaId: string, ref: HTMLImageElement | undefined | null) : string => {
if(ref == null || mangaId === DefaultManga.mangaId)
return "/blahaj.png";
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`;
}
export const GetChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
if(mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters`) as Promise<IChapter[]>;
}
export const GetDownloadedChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/Downloaded`) as Promise<IChapter[]>;
}
export const GetNotDownloadedChapters = async (apiUri: string, mangaId: string) : Promise<IChapter[]> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapters/NotDownloaded`) as Promise<IChapter[]>;
}
export const GetLatestChapterAvailable = async (apiUri: string, mangaId: string) : Promise<IChapter> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestAvailable`) as Promise<IChapter>;
}
export const GetLatestChapterDownloaded = async (apiUri: string, mangaId: string) : Promise<IChapter> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await getData(`${apiUri}/v2/Manga/${mangaId}/Chapter/LatestDownloaded`) as Promise<IChapter>;
}
export const SetIgnoreThreshold = async (apiUri: string, mangaId: string, chapterThreshold: number) : Promise<object | undefined> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
if(chapterThreshold === undefined || chapterThreshold === null)
return Promise.reject("chapterThreshold was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await patchData(`${apiUri}/v2/Manga/${mangaId}/IgnoreChaptersBefore`, chapterThreshold);
}
export const MoveFolder = async (apiUri: string, mangaId: string, newPath: string) : Promise<object | undefined> => {
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
return Promise.reject("mangaId was not provided");
if(newPath === undefined || newPath === null || newPath.length < 1)
return Promise.reject("newPath was not provided");
if(mangaId === DefaultManga.mangaId)
return Promise.reject("Default Manga was requested");
return await postData(`${apiUri}/v2/Manga/{MangaId}/MoveFolder`, {newPath});
}

View File

@ -0,0 +1,26 @@
import {getData, patchData} from './fetchApi.tsx';
import IMangaConnector from "./types/IMangaConnector.ts";
export const GetAllConnectors = async (apiUri: string) : Promise<IMangaConnector[]> => {
return await getData(`${apiUri}/v2/MangaConnector`) as Promise<IMangaConnector[]>
}
export const GetConnector = async (apiUri: string, mangaConnectorName: string) : Promise<IMangaConnector> => {
return await getData(`${apiUri}/v2/MangaConnector/${mangaConnectorName}`) as Promise<IMangaConnector>;
}
export const GetEnabledConnectors = async (apiUri: string) : Promise<IMangaConnector[]> => {
return await getData(`${apiUri}/v2/MangaConnector/enabled`) as Promise<IMangaConnector[]>
}
export const GetDisabledConnectors = async (apiUri: string) : Promise<IMangaConnector[]> => {
return await getData(`${apiUri}/v2/MangaConnector/disabled`) as Promise<IMangaConnector[]>
}
export const SetConnectorEnabled = async (apiUri: string, connectorName: string, enabled: boolean) : Promise<object | undefined> => {
if(connectorName === undefined || connectorName === null || connectorName.length < 1)
return Promise.reject("connectorName was not provided");
if(enabled === undefined || enabled === null)
return Promise.reject("enabled was not provided");
return await patchData(`${apiUri}/v2/MangaConnector/${connectorName}/SetEnabled/${enabled}`, {});
}

View File

@ -0,0 +1,45 @@
import {deleteData, getData, putData} from "./fetchApi.tsx";
import INotificationConnector from "./types/INotificationConnector.ts";
import IGotifyRecord from "./types/records/IGotifyRecord.ts";
import INtfyRecord from "./types/records/INtfyRecord.ts";
import IPushoverRecord from "./types/records/IPushoverRecord.ts";
export const GetNotificationConnectors = async (apiUri: string) : Promise<INotificationConnector[]> => {
return await getData(`${apiUri}/v2/NotificationConnector`) as Promise<INotificationConnector[]>
}
export const CreateNotificationConnector = async (apiUri: string, newConnector: INotificationConnector) : Promise<string> => {
if(newConnector === undefined || newConnector === null)
return Promise.reject("newConnector was not provided");
return await putData(`${apiUri}/v2/NotificationConnector`, newConnector) as Promise<string>;
}
export const GetNotificationConnectorWithId = async (apiUri: string, notificationConnectorId: string) : Promise<INotificationConnector> => {
if(notificationConnectorId === undefined || notificationConnectorId === null || notificationConnectorId.length < 1)
return Promise.reject("notificationConnectorId was not provided");
return await getData(`${apiUri}/v2/NotificationConnector/${notificationConnectorId}`) as Promise<INotificationConnector>;
}
export const DeleteNotificationConnectorWithId = async (apiUri: string, notificationConnectorId: string) : Promise<void> => {
if(notificationConnectorId === undefined || notificationConnectorId === null || notificationConnectorId.length < 1)
return Promise.reject("notificationConnectorId was not provided");
return await deleteData(`${apiUri}/v2/NotificationConnector/${notificationConnectorId}`);
}
export const CreateGotify = async (apiUri: string, gotify: IGotifyRecord) : Promise<string> => {
if(gotify === undefined || gotify === null)
return Promise.reject("gotify was not provided");
return await putData(`${apiUri}/v2/NotificationConnector/Gotify`, gotify) as Promise<string>;
}
export const CreateNtfy = async (apiUri: string, ntfy: INtfyRecord) : Promise<string> => {
if(ntfy === undefined || ntfy === null)
return Promise.reject("gotify was not provided");
return await putData(`${apiUri}/v2/NotificationConnector/Ntfy`, ntfy) as Promise<string>;
}
export const CreatePushover = async (apiUri: string, pushover: IPushoverRecord) : Promise<string> => {
if(pushover === undefined || pushover === null)
return Promise.reject("pushover was not provided");
return await putData(`${apiUri}/v2/NotificationConnector/Pushover`, pushover) as Promise<string>;
}

View File

@ -0,0 +1,15 @@
import IAuthor from "./types/IAuthor.ts";
import {getData} from "./fetchApi.tsx";
import ILink from "./types/ILink.ts";
export const GetAuthor = async (apiUri: string, authorId: string) : Promise<IAuthor> => {
if(authorId === undefined || authorId === null || authorId.length < 1)
return Promise.reject("authorId was not provided");
return await getData(`${apiUri}/v2/Query/Author/${authorId}`) as Promise<IAuthor>;
}
export const GetLink = async (apiUri: string, linkId: string) : Promise<ILink> => {
if(linkId === undefined || linkId === null || linkId.length < 1)
return Promise.reject("linkId was not provided");
return await getData(`${apiUri}/v2/Query/Link/${linkId}`) as Promise<ILink>;
}

View File

@ -0,0 +1,22 @@
import {getData, postData} from "./fetchApi.tsx";
import IManga from "./types/IManga.ts";
export const SearchName = async (apiUri: string, name: string) : Promise<IManga[]> => {
if(name === undefined || name === null || name.length < 1)
return Promise.reject("name was not provided");
return await postData(`${apiUri}/v2/Search/Name`, name) as Promise<IManga[]>;
}
export const SearchNameOnConnector = async (apiUri: string, connectorName: string, name: string) : Promise<IManga[]> => {
if(connectorName === undefined || connectorName === null || connectorName.length < 1)
return Promise.reject("connectorName was not provided");
if(name === undefined || name === null || name.length < 1)
return Promise.reject("name was not provided");
return await getData(`${apiUri}/v2/Search/${connectorName}/${name}`) as Promise<IManga[]>;
}
export const SearchUrl = async (apiUri: string, url: string) : Promise<IManga> => {
if(url === undefined || url === null || url.length < 1)
return Promise.reject("name was not provided");
return await postData(`${apiUri}/v2/Search/Url`, url) as Promise<IManga>;
}

View File

@ -0,0 +1,89 @@
import {createContext} from "react";
export const ApiUriContext = createContext<string>("");
export function getData(uri: string) : Promise<object | undefined> {
return makeRequestWrapper("GET", uri, null);
}
export function postData(uri: string, content?: object | string | number | boolean | null) : Promise<object | undefined> {
return makeRequestWrapper("POST", uri, content);
}
export function deleteData(uri: string) : Promise<void> {
return makeRequestWrapper("DELETE", uri, null) as Promise<void>;
}
export function patchData(uri: string, content: object | string | number | boolean) : Promise<object | undefined> {
return makeRequestWrapper("patch", uri, content);
}
export function putData(uri: string, content: object | string | number | boolean) : Promise<object | undefined> {
return makeRequestWrapper("PUT", uri, content);
}
function makeRequestWrapper(method: string, uri: string, content?: object | string | number | null | boolean) : Promise<object | undefined>{
return makeRequest(method, uri, content)
.then((result) => result as Promise<object>)
.catch((e) => {
console.warn(e);
return Promise.reject(e);
});
}
let currentlyRequestedEndpoints: string[] = [];
function makeRequest(method: string, uri: string, content?: object | string | number | null | boolean) : Promise<object | void> {
const id = method + uri;
if(currentlyRequestedEndpoints.find(x => x == id) != undefined)
return Promise.reject(`DO NOT REPORT! Already requested: ${method} ${uri}`);
currentlyRequestedEndpoints.push(id);
return fetch(uri,
{
method: method,
headers : {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: content ? JSON.stringify(content) : null
})
.then(function(response){
if(!response.ok){
if(response.status === 503){
currentlyRequestedEndpoints.splice(currentlyRequestedEndpoints.indexOf(id), 1)
let retryHeaderVal = response.headers.get("Retry-After");
let seconds = 10;
if(retryHeaderVal === null){
return response.text().then(text => {
seconds = parseInt(text);
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
.then(() => {
return makeRequest(method, uri, content);
});
});
}else {
seconds = parseInt(retryHeaderVal);
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
.then(() => {
return makeRequest(method, uri, content);
});
}
}else
throw new Error(response.statusText);
}
let json = response.json();
return json.then((json) => json).catch(() => null);
})
.catch(function(err : Error){
console.error(`Error ${method}ing Data ${uri}\n${err}`);
return Promise.reject();
}).finally(() => currentlyRequestedEndpoints.splice(currentlyRequestedEndpoints.indexOf(id), 1));
}
export function isValidUri(uri: string) : boolean{
try {
new URL(uri);
return true;
} catch (err) {
return false;
}
}

View File

@ -0,0 +1,24 @@
import {ColorPaletteProp} from "@mui/joy";
export enum MangaReleaseStatus {
Continuing = "Continuing",
Completed = "Completed",
OnHiatus = "OnHiatus",
Cancelled = "Cancelled",
Unreleased = "Unreleased",
}
export function ReleaseStatusToPalette(status: MangaReleaseStatus): ColorPaletteProp {
switch (status) {
case MangaReleaseStatus.Continuing:
return "success";
case MangaReleaseStatus.Completed:
return "primary";
case MangaReleaseStatus.Cancelled:
return "danger";
case MangaReleaseStatus.Unreleased:
return "neutral";
case MangaReleaseStatus.OnHiatus:
return "warning";
}
}

View File

@ -0,0 +1,8 @@
export enum RequestLimitType {
Default = "Default",
MangaDexFeed = "MangaDexFeed",
MangaImage = "MangaImage",
MangaCover = "MangaCover",
MangaDexImage = "MangaDexImage",
MangaInfo = "MangaInfo"
}

View File

@ -0,0 +1,4 @@
export default interface IAuthor {
authorId: string;
authorName: string;
}

View File

@ -0,0 +1,19 @@
export default interface IBackendSettings {
downloadLocation: string;
workingDirectory: string;
userAgent: string;
aprilFoolsMode: boolean;
requestLimits: {
Default: number,
MangaInfo: number,
MangaDexFeed: number,
MangaDexImage: number,
MangaImage: number,
MangaCover: number,
};
compression: number;
bwImages: boolean;
startNewJobTimeoutMs: number;
chapterNamingScheme: string;
flareSolverrUrl: string;
}

View File

@ -0,0 +1,11 @@
export default interface IChapter{
chapterId: string;
parentMangaId: string;
volumeNumber: number | null;
chapterNumber: string;
url: string;
title: string | null;
fileName: string | null;
downloaded: boolean;
fullArchiveFilePath: string;
}

View File

@ -0,0 +1,4 @@
export default interface IFrontendSettings {
jobInterval: Date;
apiUri: string;
}

View File

@ -0,0 +1,11 @@
export default interface ILibraryConnector {
libraryConnectorId: string;
libraryType: LibraryType;
baseUrl: string;
auth: string;
}
export enum LibraryType {
Komga = "Komga",
Kavita = "Kavita"
}

View File

@ -0,0 +1,5 @@
export default interface ILink {
linkId: string;
linkProvider: string;
linkUrl: string;
}

View File

@ -0,0 +1,5 @@
export default interface ILocalLibrary {
localLibraryId: string;
basePath: string;
libraryName: string;
}

View File

@ -0,0 +1,45 @@
import {MangaReleaseStatus} from "./EnumMangaReleaseStatus";
import IAuthor from "./IAuthor.ts";
import IMangaAltTitle from "./IMangaAltTitle.ts";
import IMangaTag from "./IMangaTag.ts";
import ILink from "./ILink.ts";
export default interface IManga{
mangaId: string;
idOnConnectorSite: string;
name: string;
description: string;
websiteUrl: string;
releaseStatus: MangaReleaseStatus;
libraryId: string | null;
mangaConnectorName: string;
authors: IAuthor[] | null;
mangaTags: IMangaTag[] | null;
links: ILink[] | null;
altTitles: IMangaAltTitle[] | null;
ignoreChaptersBefore: number;
directoryName: string;
year: number | null;
originalLanguage: string | null;
chapterIds: string[] | null;
}
export const DefaultManga : IManga = {
mangaId: "Loading",
idOnConnectorSite: "Loading",
name: "Loading",
description: "Loading",
websiteUrl: "",
releaseStatus: MangaReleaseStatus.Continuing,
libraryId: null,
mangaConnectorName: "Loading",
authors: null,
mangaTags: null,
links: null,
altTitles: null,
ignoreChaptersBefore: 0,
directoryName: "",
year: 1999,
originalLanguage: "en",
chapterIds: null
}

View File

@ -0,0 +1,5 @@
export default interface IMangaAltTitle {
altTitleId: string;
language: string;
title: string;
}

View File

@ -0,0 +1,7 @@
export default interface IMangaConnector {
name: string;
supportedLanguages: string[];
iconUrl: string;
baseUris: string[];
enabled: boolean;
}

View File

@ -0,0 +1,3 @@
export default interface IMangaTag {
tag: string;
}

View File

@ -0,0 +1,7 @@
export default interface INotificationConnector {
name: string;
url: string;
headers: Record<string, string>[];
httpMethod: string;
body: string;
}

View File

@ -0,0 +1,8 @@
export default interface IRequestLimits {
Default: number;
MangaDexFeed: number;
MangaImage: number;
MangaCover: number;
MangaDexImage: number;
MangaInfo: number;
}

View File

@ -0,0 +1,5 @@
import IJobWithMangaId from "./IJobWithMangaId.ts";
export default interface IDownloadAvailableChaptersJob extends IJobWithMangaId {
}

View File

@ -0,0 +1,5 @@
import IJobWithMangaId from "./IJobWithMangaId.ts";
export default interface IDownloadMangaCoverJob extends IJobWithMangaId {
}

View File

@ -0,0 +1,5 @@
import IJobWithChapterId from "./IJobWithChapterId.tsx";
export default interface IDownloadSingleChapterJob extends IJobWithChapterId {
}

View File

@ -0,0 +1,36 @@
export default interface IJob{
jobId: string;
parentJobId: string | null;
jobType: JobType;
recurrenceMs: number;
lastExecution: Date;
nextExecution: Date;
state: JobState;
enabled: boolean;
}
export enum JobType {
DownloadSingleChapterJob = "DownloadSingleChapterJob",
DownloadAvailableChaptersJob = "DownloadAvailableChaptersJob",
DownloadMangaCoverJob = "DownloadMangaCoverJob",
RetrieveChaptersJob = "RetrieveChaptersJob",
UpdateChaptersDownloadedJob = "UpdateChaptersDownloadedJob",
MoveMangaLibraryJob = "MoveMangaLibraryJob",
UpdateCoverJob = "UpdateCoverJob"
}
export function JobTypeToString(job: JobType | string): string {
return job.replace(/([A-Z])/g, ' $1').replace("Job", "").trim();
}
export enum JobState {
FirstExecution = "FirstExecution",
Running = "Running",
Completed = "Completed",
CompletedWaiting = "CompletedWaiting",
Failed = "Failed"
}
export function JobStateToString(state: JobState | string): string {
return state.replace(/([A-Z])/g, ' $1').trim();
}

View File

@ -0,0 +1,5 @@
import IJob from "./IJob.ts";
export default interface IJobWithChapterId extends IJob {
chapterId: string;
}

View File

@ -0,0 +1,5 @@
import IJob from "./IJob.ts";
export default interface IJobWithMangaId extends IJob {
mangaId: string;
}

View File

@ -0,0 +1,6 @@
import IJob from "./IJob";
export default interface IMoveFileOrFolderJob extends IJob {
fromLocation: string;
toLocation: string;
}

View File

@ -0,0 +1,5 @@
import IJobWithMangaId from "./IJobWithMangaId.ts";
export default interface IMoveMangaLibraryJob extends IJobWithMangaId {
ToLibraryId: string;
}

Some files were not shown because too many files have changed in this diff Show More