Compare commits

..

15 Commits

Author SHA1 Message Date
rov
8b27e56758 Merge pull request 'feat(provider-import): unify provider import dialogs' (#15) from feature/provider-import into main
Reviewed-on: #15
2025-12-14 00:53:25 -03:00
4d77ce9c49 feat(provider-import): unify provider import dialogs 2025-12-14 00:45:33 -03:00
rov
1a0f730e72 Merge pull request 'feature(provider): add provider status and update manga list functionality' (#14) from feature/inactive-provider into main
Reviewed-on: #14
2025-11-29 22:21:34 -03:00
81fcff40cb feature(provider): add provider status and update manga list functionality 2025-11-29 22:13:06 -03:00
rov
e9035fa54e Merge pull request 'chore(pipeline): migrating from woodpecker ci/cd to komodo stack deployment' (#13) from feature/komodo into main
Reviewed-on: #13
2025-11-15 20:56:31 -03:00
2b0f63b9ac chore(pipeline): migrating from woodpecker ci/cd to komodo stack deployment 2025-11-15 20:49:21 -03:00
rov
5a06d2d738 Merge pull request 'feature(import): implement Bato manga import functionality' (#12) from feature/bato into main
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
Reviewed-on: #12
2025-11-13 23:12:09 -03:00
aca0d114fb feature(import): implement Bato manga import functionality
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
ci/woodpecker/pr/pipeline Pipeline was successful
2025-11-13 23:04:38 -03:00
rov
1cdfc905e4 Merge pull request 'feature(manga): add follow and unfollow functionality for manga' (#11) from feature/manga-follow into main
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
Reviewed-on: #11
2025-11-12 22:04:22 -03:00
246c6023d7 feature(manga): add follow and unfollow functionality for manga
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
2025-11-12 20:40:20 -03:00
rov
5f33b87ece Merge pull request 'chore(env): add local environment configuration for API base URLs' (#10) from chore/env into main
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
Reviewed-on: #10
2025-11-10 21:37:26 -03:00
002854d65c chore(env): add local environment configuration for API base URLs
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
ci/woodpecker/pr/pipeline Pipeline was successful
2025-11-10 21:22:16 -03:00
rov
963b6e30db Merge pull request 'fix(import): refine MangaDex ID validation logic in import dialog' (#9) from fix/manga-dex into main
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
Reviewed-on: #9
2025-11-02 15:15:03 -03:00
rov
5d87efb88e Merge pull request 'fix(import): refine MangaDex ID validation logic in import dialog' (#8) from fix/manga-dex into main
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
Reviewed-on: #8
2025-11-02 15:05:53 -03:00
rov
313f3cdfcf Merge pull request 'fix(import): refine MangaDex ID validation logic in import dialog' (#7) from fix/manga-dex into main
Some checks are pending
ci/woodpecker/push/pipeline Pipeline is running
Reviewed-on: #7
2025-11-02 15:01:13 -03:00
21 changed files with 1546 additions and 532 deletions

2
.env.local Normal file
View File

@ -0,0 +1,2 @@
VITE_API_BASE_URL=http://localhost:8080
VITE_OMV_BASE_URL=http://omv2.badger-pirarucu.ts.net:9000/mangamochi

View File

@ -1,61 +0,0 @@
# .pipeline.yml
# -----------------
when:
event: [ push, pull_request ]
steps:
- name: publish-image
image: woodpeckerci/plugin-docker-buildx
settings:
platforms: linux/amd64
repo: git.badger-pirarucu.ts.net/mangamochi/frontend
registry: git.badger-pirarucu.ts.net
dockerfile: Dockerfile
context: .
username:
from_secret: DOCKER_USER
password:
from_secret: DOCKER_PASSWORD
build_args:
VITE_API_BASE_URL:
from_secret: NEXT_PUBLIC_API_BASE_URL
VITE_OMV_BASE_URL:
from_secret: NEXT_PUBLIC_OMV_BASE_URL
tags:
- latest
- ${CI_COMMIT_SHA}
when:
event: [ push ]
branch: [ main ]
- name: deploy
depends_on: [ publish-image ]
image: alpine:3.20
environment:
DEPLOY_USER: rov
DEPLOY_HOST: mangamochi.badger-pirarucu.ts.net
DEPLOY_PORT: 22
IMAGE: git.badger-pirarucu.ts.net/mangamochi/frontend:${CI_COMMIT_SHA}
DEPLOY_SSH_KEY:
from_secret: DEPLOY_SSH_KEY
commands:
- echo "🚀 Deploying Next.js app to $DEPLOY_HOST...."
- apk add --no-cache openssh-client docker-cli
- mkdir -p ~/.ssh
- echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -p $DEPLOY_PORT $DEPLOY_HOST >> ~/.ssh/known_hosts
- >
ssh -p $DEPLOY_PORT $DEPLOY_USER@$DEPLOY_HOST "
docker pull $IMAGE &&
docker stop mangamochi-frontend 2>/dev/null || true &&
docker rm mangamochi-frontend 2>/dev/null || true &&
docker run -d --name mangamochi-frontend \
--restart always \
-p 80:80 \
$IMAGE
"
when:
event: [ push ]
branch: [ main ]

237
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
@ -105,6 +106,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@ -176,6 +178,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@ -1157,9 +1160,9 @@
} }
}, },
"node_modules/@ibm-cloud/openapi-ruleset": { "node_modules/@ibm-cloud/openapi-ruleset": {
"version": "1.33.3", "version": "1.33.4",
"resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset/-/openapi-ruleset-1.33.3.tgz", "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset/-/openapi-ruleset-1.33.4.tgz",
"integrity": "sha512-lOxglXIzUZwsw5WsbgZraxxzAYMdXYyiMNOioxYJYTd55ZuN4XEERoPdV5v1oPTdKedHEUSQu5siiSHToENFdA==", "integrity": "sha512-fF1/Uk8jbQIAnWueUxHan6ywORXwQbMvZ6hGjpY77sXLrgcVjWsbJPndeakJxOsTy3pxYcw9cvmkEv+IcIK+nQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@ibm-cloud/openapi-ruleset-utilities": "1.9.0", "@ibm-cloud/openapi-ruleset-utilities": "1.9.0",
@ -1173,7 +1176,7 @@
"loglevel": "^1.9.2", "loglevel": "^1.9.2",
"loglevel-plugin-prefix": "0.8.4", "loglevel-plugin-prefix": "0.8.4",
"minimatch": "^6.2.0", "minimatch": "^6.2.0",
"validator": "^13.11.0" "validator": "^13.15.23"
}, },
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
@ -1329,27 +1332,27 @@
} }
}, },
"node_modules/@orval/angular": { "node_modules/@orval/angular": {
"version": "7.15.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/@orval/angular/-/angular-7.15.0.tgz", "resolved": "https://registry.npmjs.org/@orval/angular/-/angular-7.17.0.tgz",
"integrity": "sha512-CVQfohMdl7lU0StRlGJ0RfuhmsV+DrAy0x2sU8Mo6+Jb0UYGPhh/lQ+umsAHrCn5sqmoR8AEBoRDSYiD83rw/w==", "integrity": "sha512-3DnUU/2vUhKC33bmM2xMGRbbJRs8Qvubvjg+0QGh3AxedOYWRcLTxsuZNyr6n4y2TomrsbB1rFMTveTotE2VuA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@orval/core": "7.15.0" "@orval/core": "7.17.0"
} }
}, },
"node_modules/@orval/axios": { "node_modules/@orval/axios": {
"version": "7.15.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/@orval/axios/-/axios-7.15.0.tgz", "resolved": "https://registry.npmjs.org/@orval/axios/-/axios-7.17.0.tgz",
"integrity": "sha512-NseXzeQpIfycvKaTr+g/pJoIGOqbR5IPWKwWtUGRal78k48NaVKlC5TIYR0PwyCUvxhli4rKPUsH5hsdUBZu/A==", "integrity": "sha512-qbTOOOGjtfFDgpY1pHJNEO71CybSQiPJxuspVJDnWeoR7cwxOuZuWLWX3QE0bZ/2NHUg11rcuHwzzvPHGYmb0w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@orval/core": "7.15.0" "@orval/core": "7.17.0"
} }
}, },
"node_modules/@orval/core": { "node_modules/@orval/core": {
"version": "7.15.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/@orval/core/-/core-7.15.0.tgz", "resolved": "https://registry.npmjs.org/@orval/core/-/core-7.17.0.tgz",
"integrity": "sha512-jkEZQKvVZN6++ji6PicTbPHo/bzhvDqg2GIXL9u2t6rFgFwowZ92CP3v+S5bku+Y82d+wDYRUBCr1fVg/u9dzg==", "integrity": "sha512-oLHJitYNUbPYCijKt77az9Q7PnQE8ga79hVUt46c5cL4dXjjbRMLXSCIxB4M5lbu4D9q4G6ZgjCAxv+Fr7wGRA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^12.1.0", "@apidevtools/swagger-parser": "^12.1.0",
@ -1374,83 +1377,89 @@
} }
}, },
"node_modules/@orval/fetch": { "node_modules/@orval/fetch": {
"version": "7.15.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-7.15.0.tgz", "resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-7.17.0.tgz",
"integrity": "sha512-GwQohzMmwuSSbyu68z0gShGJIeRRYFtBJzugZCmY7z99A1pksaWvierWIHc7tmp2jXeOrodat7h/Ju3Hsw6WAQ==", "integrity": "sha512-VZuSKa2tMhHyL4BKiPyXmNj0Z5jF92lisTgWdm24WdmFfrzeGCHDyBbim6ivOMnrcvf0v7o16EeaTdMeNiaGdQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@orval/core": "7.15.0", "@orval/core": "7.17.0",
"openapi3-ts": "4.5.0" "openapi3-ts": "4.5.0"
} }
}, },
"node_modules/@orval/hono": { "node_modules/@orval/hono": {
"version": "7.15.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/@orval/hono/-/hono-7.15.0.tgz", "resolved": "https://registry.npmjs.org/@orval/hono/-/hono-7.17.0.tgz",
"integrity": "sha512-lw8UYTEjQYUq7Fswh0INXyWpOaj9V1wTfXh5KL2xMjI2qhuyAoQhCkG9M1a07X7cVrK0RxSG5ZkbBeWWBeCjDA==", "integrity": "sha512-/78Gb346+I0jPLML/s/QK7O8sJoiCIQxKT0sJ3YVXPLSKQ4aBEYGcXthAW/LwJTZWz8Rm0vGGnz4svra3gafwg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@orval/core": "7.15.0", "@orval/core": "7.17.0",
"@orval/zod": "7.15.0", "@orval/zod": "7.17.0",
"fs-extra": "^11.3.2", "fs-extra": "^11.3.2",
"lodash.uniq": "^4.5.0", "lodash.uniq": "^4.5.0",
"openapi3-ts": "4.5.0" "openapi3-ts": "4.5.0"
} }
}, },
"node_modules/@orval/mcp": { "node_modules/@orval/mcp": {
"version": "7.15.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/@orval/mcp/-/mcp-7.15.0.tgz", "resolved": "https://registry.npmjs.org/@orval/mcp/-/mcp-7.17.0.tgz",
"integrity": "sha512-unq34kRmLNMCG/9q6Kl5bilHjWIapt2dQfdoeb+02N/Pt870hulgaH3ERFOl23JPOUBW9vj2itbE+Qin5z2/2w==", "integrity": "sha512-izVIA3XgBpdQ6u2VLD7lxXbksq2K0wf8ypmQAufd5SewSoyEPcu8jIlLovGLTI5WiBr0pJnHLoltJ4Pl/zkZ0A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@orval/core": "7.15.0", "@orval/core": "7.17.0",
"@orval/fetch": "7.15.0", "@orval/fetch": "7.17.0",
"@orval/zod": "7.15.0", "@orval/zod": "7.17.0",
"openapi3-ts": "4.5.0" "openapi3-ts": "4.5.0"
} }
}, },
"node_modules/@orval/mock": { "node_modules/@orval/mock": {
"version": "7.15.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/@orval/mock/-/mock-7.15.0.tgz", "resolved": "https://registry.npmjs.org/@orval/mock/-/mock-7.17.0.tgz",
"integrity": "sha512-3oe6jfYsOWQVm/3A7T2ABjfjkdXTrNwB+KYn/4IToWtzViQjPQxderrNhB8yH8PmEzqRxkz8vGfqpGSUIfRikQ==", "integrity": "sha512-A/A/50XXBgidhTlQDyPQp6nIUfrP27GagyEQ70ztXS5Z9y1WJe7K5ZjjCnISOc7cFTZJmsYKvuCUo4ptfTm1bA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@orval/core": "7.15.0", "@orval/core": "7.17.0",
"openapi3-ts": "4.5.0" "openapi3-ts": "4.5.0"
} }
}, },
"node_modules/@orval/query": { "node_modules/@orval/query": {
"version": "7.15.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/@orval/query/-/query-7.15.0.tgz", "resolved": "https://registry.npmjs.org/@orval/query/-/query-7.17.0.tgz",
"integrity": "sha512-0SmpZNfisOU4piEWpfynl+O2EKRiH2FS6j5cGewDpsAjmZN7Pqg35h9EGvtYUgFisURH5z8AzB8P8hfVeNjxDA==", "integrity": "sha512-h/XZRpOLOewIPa/3uSk68M6CSgylrGSUxHux9uEH3P2nYWjwYV+GxuOUptzuONA37LGd76mgytW6tK27VmYeCA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@orval/core": "7.15.0", "@orval/core": "7.17.0",
"@orval/fetch": "7.15.0", "@orval/fetch": "7.17.0",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"lodash.omitby": "^4.6.0" "lodash.omitby": "^4.6.0"
} }
}, },
"node_modules/@orval/swr": { "node_modules/@orval/swr": {
"version": "7.15.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/@orval/swr/-/swr-7.15.0.tgz", "resolved": "https://registry.npmjs.org/@orval/swr/-/swr-7.17.0.tgz",
"integrity": "sha512-papSt3cuhxSwIj3xoHz1JYt8Btt+qGHMsjKFoDJK4EYaotZRp5OYSElojJl8aOTyKEYc+3elQNkncReF24azfQ==", "integrity": "sha512-ZC5ZjzILWt8WE3V4gWRIg9XWHjQBubUJzin67aKERlybPN+sqMdgMEl9/XL+emUItkAIdex9cCaDHZwqjrmbKg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@orval/core": "7.15.0", "@orval/core": "7.17.0",
"@orval/fetch": "7.15.0" "@orval/fetch": "7.17.0"
} }
}, },
"node_modules/@orval/zod": { "node_modules/@orval/zod": {
"version": "7.15.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/@orval/zod/-/zod-7.15.0.tgz", "resolved": "https://registry.npmjs.org/@orval/zod/-/zod-7.17.0.tgz",
"integrity": "sha512-E86N+NeZI644zaHVDvDSrwkHTtreatBsnkWkktZVklE4C3eP99h0SmyiUm8l6miA2jXJIh6nF+/sbWdrbkmU7A==", "integrity": "sha512-ldwGUR4K0YIay+4UGR7ykTYD99Cs+CHIAOAVVny8/h9dU0CD/6sxomHuCzrC0Z2QxVZ4rL7k8g10Kffmp+dHBw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@orval/core": "7.15.0", "@orval/core": "7.17.0",
"lodash.uniq": "^4.5.0", "lodash.uniq": "^4.5.0",
"openapi3-ts": "4.5.0" "openapi3-ts": "4.5.0"
} }
}, },
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/primitive": { "node_modules/@radix-ui/primitive": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
@ -1955,6 +1964,49 @@
} }
} }
}, },
"node_modules/@radix-ui/react-select": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-visually-hidden": "1.2.3",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator": { "node_modules/@radix-ui/react-separator": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
@ -2209,6 +2261,29 @@
} }
} }
}, },
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/rect": { "node_modules/@radix-ui/rect": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
@ -2686,6 +2761,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@ -2770,6 +2846,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@ -2901,6 +2978,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@ -3356,6 +3434,7 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@ -3366,6 +3445,7 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@ -3428,6 +3508,7 @@
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2", "@typescript-eslint/types": "8.46.2",
@ -3691,6 +3772,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3969,6 +4051,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.19", "baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751", "caniuse-lite": "^1.0.30001751",
@ -4168,6 +4251,7 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20" "node": ">=20"
} }
@ -4441,9 +4525,9 @@
} }
}, },
"node_modules/es-abstract": { "node_modules/es-abstract": {
"version": "1.24.0", "version": "1.24.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
"integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"array-buffer-byte-length": "^1.0.2", "array-buffer-byte-length": "^1.0.2",
@ -4667,6 +4751,7 @@
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -5893,9 +5978,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@ -5909,6 +5994,7 @@
"resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz",
"integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 10.16.0" "node": ">= 10.16.0"
} }
@ -6832,23 +6918,23 @@
} }
}, },
"node_modules/orval": { "node_modules/orval": {
"version": "7.15.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/orval/-/orval-7.15.0.tgz", "resolved": "https://registry.npmjs.org/orval/-/orval-7.17.0.tgz",
"integrity": "sha512-uw03ULVDLX2coGbjZalq4sKQj2io6eyhJOqiIFcY76VqiJz9GUxrBvQwaFwyxOEUfy9EoI25c+clAjpYggNeNw==", "integrity": "sha512-iBqZC7HpSSL1CJ9jRCD+5vCYpedd03Udh+izcyFnWyVUN0ywuzGonizJgl5iGTgoe+VXsMM7ndV5h+DkghreMg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^12.1.0", "@apidevtools/swagger-parser": "^12.1.0",
"@commander-js/extra-typings": "^14.0.0", "@commander-js/extra-typings": "^14.0.0",
"@orval/angular": "7.15.0", "@orval/angular": "7.17.0",
"@orval/axios": "7.15.0", "@orval/axios": "7.17.0",
"@orval/core": "7.15.0", "@orval/core": "7.17.0",
"@orval/fetch": "7.15.0", "@orval/fetch": "7.17.0",
"@orval/hono": "7.15.0", "@orval/hono": "7.17.0",
"@orval/mcp": "7.15.0", "@orval/mcp": "7.17.0",
"@orval/mock": "7.15.0", "@orval/mock": "7.17.0",
"@orval/query": "7.15.0", "@orval/query": "7.17.0",
"@orval/swr": "7.15.0", "@orval/swr": "7.17.0",
"@orval/zod": "7.15.0", "@orval/zod": "7.17.0",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"chokidar": "^4.0.3", "chokidar": "^4.0.3",
"commander": "^14.0.1", "commander": "^14.0.1",
@ -6856,7 +6942,8 @@
"execa": "^5.1.1", "execa": "^5.1.1",
"find-up": "5.0.0", "find-up": "5.0.0",
"fs-extra": "^11.3.2", "fs-extra": "^11.3.2",
"js-yaml": "4.1.0", "jiti": "^2.6.1",
"js-yaml": "4.1.1",
"lodash.uniq": "^4.5.0", "lodash.uniq": "^4.5.0",
"openapi3-ts": "4.5.0", "openapi3-ts": "4.5.0",
"string-argv": "^0.3.2", "string-argv": "^0.3.2",
@ -7083,6 +7170,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -7092,6 +7180,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@ -7104,6 +7193,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz",
"integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@ -7942,6 +8032,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -8108,6 +8199,7 @@
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.14.tgz", "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.14.tgz",
"integrity": "sha512-ftJYPvpVfQvFzpkoSfHLkJybdA/geDJ8BGQt/ZnkkhnBYoYW6lBgPQXu6vqLxO4X75dA55hX8Af847H5KXlEFA==", "integrity": "sha512-ftJYPvpVfQvFzpkoSfHLkJybdA/geDJ8BGQt/ZnkkhnBYoYW6lBgPQXu6vqLxO4X75dA55hX8Af847H5KXlEFA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@gerrit0/mini-shiki": "^3.12.0", "@gerrit0/mini-shiki": "^3.12.0",
"lunr": "^2.3.9", "lunr": "^2.3.9",
@ -8179,6 +8271,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -8371,9 +8464,9 @@
} }
}, },
"node_modules/validator": { "node_modules/validator": {
"version": "13.15.20", "version": "13.15.23",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz",
"integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"
@ -8384,6 +8477,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@ -8475,6 +8569,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },

View File

@ -16,6 +16,7 @@
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",

View File

@ -1,30 +1,27 @@
/** /**
* Generated by orval v7.15.0 🍺 * Generated by orval v7.17.0 🍺
* Do not edit manually. * Do not edit manually.
* OpenAPI definition * OpenAPI definition
* OpenAPI spec version: v0 * OpenAPI spec version: v0
*/ */
export interface UpdateMangaDataCommand {
mangaId?: number;
}
export interface DefaultResponseDTOVoid { export interface DefaultResponseDTOVoid {
timestamp?: string; timestamp?: string;
data?: unknown; data?: unknown;
message?: string; message?: string;
} }
export interface ImportMangaDexRequestDTO { export interface ImportRequestDTO {
metadataId?: string;
id: string; id: string;
} }
export interface DefaultResponseDTOImportMangaDexResponseDTO { export interface DefaultResponseDTOImportMangaResponseDTO {
timestamp?: string; timestamp?: string;
data?: ImportMangaDexResponseDTO; data?: ImportMangaResponseDTO;
message?: string; message?: string;
} }
export interface ImportMangaDexResponseDTO { export interface ImportMangaResponseDTO {
id: number; id: number;
} }
@ -66,6 +63,21 @@ export interface AuthenticationRequestDTO {
password: string; password: string;
} }
export interface DefaultResponseDTOProviderListDTO {
timestamp?: string;
data?: ProviderListDTO;
message?: string;
}
export interface ProviderDTO {
id: number;
name: string;
}
export interface ProviderListDTO {
providers: ProviderDTO[];
}
export interface DefaultResponseDTOPageMangaListDTO { export interface DefaultResponseDTOPageMangaListDTO {
timestamp?: string; timestamp?: string;
data?: PageMangaListDTO; data?: PageMangaListDTO;
@ -90,29 +102,29 @@ export interface MangaListDTO {
export interface PageMangaListDTO { export interface PageMangaListDTO {
totalPages?: number; totalPages?: number;
totalElements?: number; totalElements?: number;
size?: number;
content?: MangaListDTO[];
number?: number;
pageable?: PageableObject; pageable?: PageableObject;
first?: boolean; first?: boolean;
last?: boolean; last?: boolean;
size?: number;
content?: MangaListDTO[];
number?: number;
sort?: SortObject; sort?: SortObject;
numberOfElements?: number; numberOfElements?: number;
empty?: boolean; empty?: boolean;
} }
export interface PageableObject { export interface PageableObject {
offset?: number;
pageNumber?: number; pageNumber?: number;
pageSize?: number; pageSize?: number;
paged?: boolean; paged?: boolean;
offset?: number;
sort?: SortObject; sort?: SortObject;
unpaged?: boolean; unpaged?: boolean;
} }
export interface SortObject { export interface SortObject {
empty?: boolean;
sorted?: boolean; sorted?: boolean;
empty?: boolean;
unsorted?: boolean; unsorted?: boolean;
} }
@ -152,12 +164,24 @@ export interface MangaDTO {
score: number; score: number;
providers: MangaProviderDTO[]; providers: MangaProviderDTO[];
chapterCount: number; chapterCount: number;
favorite: boolean;
following: boolean;
} }
export type MangaProviderDTOProviderStatus = typeof MangaProviderDTOProviderStatus[keyof typeof MangaProviderDTOProviderStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const MangaProviderDTOProviderStatus = {
ACTIVE: 'ACTIVE',
INACTIVE: 'INACTIVE',
} as const;
export interface MangaProviderDTO { export interface MangaProviderDTO {
id: number; id: number;
/** @minLength 1 */ /** @minLength 1 */
providerName: string; providerName: string;
providerStatus: MangaProviderDTOProviderStatus;
chaptersAvailable: number; chaptersAvailable: number;
chaptersDownloaded: number; chaptersDownloaded: number;
supportsChapterFetch: boolean; supportsChapterFetch: boolean;
@ -173,6 +197,8 @@ export interface MangaChapterImagesDTO {
id: number; id: number;
/** @minLength 1 */ /** @minLength 1 */
mangaTitle: string; mangaTitle: string;
previousChapterId?: number;
nextChapterId?: number;
chapterImageKeys: string[]; chapterImageKeys: string[];
} }
@ -231,6 +257,14 @@ importReviewId: number;
malId: string; malId: string;
}; };
export type UpdateProviderMangaListParams = {
providerId: number;
};
export type GetProvidersParams = {
manualImport?: boolean;
};
export type GetMangasParams = { export type GetMangasParams = {
searchQuery?: string; searchQuery?: string;
genreIds?: number[]; genreIds?: number[];

View File

@ -1,5 +1,5 @@
/** /**
* Generated by orval v7.15.0 🍺 * Generated by orval v7.17.0 🍺
* Do not edit manually. * Do not edit manually.
* OpenAPI definition * OpenAPI definition
* OpenAPI spec version: v0 * OpenAPI spec version: v0

View File

@ -1,86 +0,0 @@
/**
* Generated by orval v7.15.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import {
useMutation
} from '@tanstack/react-query';
import type {
MutationFunction,
QueryClient,
UseMutationOptions,
UseMutationResult
} from '@tanstack/react-query';
import type {
UpdateMangaDataCommand
} from '../api.schemas';
import { customInstance } from '../../api';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
export const sendRecord = (
updateMangaDataCommand: UpdateMangaDataCommand,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<string>(
{url: `/records`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: updateMangaDataCommand, signal
},
options);
}
export const getSendRecordMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof sendRecord>>, TError,{data: UpdateMangaDataCommand}, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof sendRecord>>, TError,{data: UpdateMangaDataCommand}, TContext> => {
const mutationKey = ['sendRecord'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof sendRecord>>, {data: UpdateMangaDataCommand}> = (props) => {
const {data} = props ?? {};
return sendRecord(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type SendRecordMutationResult = NonNullable<Awaited<ReturnType<typeof sendRecord>>>
export type SendRecordMutationBody = UpdateMangaDataCommand
export type SendRecordMutationError = unknown
export const useSendRecord = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof sendRecord>>, TError,{data: UpdateMangaDataCommand}, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof sendRecord>>,
TError,
{data: UpdateMangaDataCommand},
TContext
> => {
const mutationOptions = getSendRecordMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}

View File

@ -1,5 +1,5 @@
/** /**
* Generated by orval v7.15.0 🍺 * Generated by orval v7.17.0 🍺
* Do not edit manually. * Do not edit manually.
* OpenAPI definition * OpenAPI definition
* OpenAPI spec version: v0 * OpenAPI spec version: v0

View File

@ -1,5 +1,5 @@
/** /**
* Generated by orval v7.15.0 🍺 * Generated by orval v7.17.0 🍺
* Do not edit manually. * Do not edit manually.
* OpenAPI definition * OpenAPI definition
* OpenAPI spec version: v0 * OpenAPI spec version: v0

View File

@ -0,0 +1,345 @@
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import {
useMutation
} from '@tanstack/react-query';
import type {
MutationFunction,
QueryClient,
UseMutationOptions,
UseMutationResult
} from '@tanstack/react-query';
import type {
DefaultResponseDTOVoid,
UpdateProviderMangaListParams
} from '../api.schemas';
import { customInstance } from '../../api';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* Trigger user follow update
* @summary Trigger user follow update
*/
export const userFollowUpdate = (
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/management/user-follow`, method: 'POST', signal
},
options);
}
export const getUserFollowUpdateMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof userFollowUpdate>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof userFollowUpdate>>, TError,void, TContext> => {
const mutationKey = ['userFollowUpdate'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof userFollowUpdate>>, void> = () => {
return userFollowUpdate(requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type UserFollowUpdateMutationResult = NonNullable<Awaited<ReturnType<typeof userFollowUpdate>>>
export type UserFollowUpdateMutationError = unknown
/**
* @summary Trigger user follow update
*/
export const useUserFollowUpdate = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof userFollowUpdate>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof userFollowUpdate>>,
TError,
void,
TContext
> => {
const mutationOptions = getUserFollowUpdateMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* Queue the retrieval of the manga list for a specific provider
* @summary Queue update provider manga list
*/
export const updateProviderMangaList = (
params: UpdateProviderMangaListParams,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/management/update-provider-manga-list`, method: 'POST',
params, signal
},
options);
}
export const getUpdateProviderMangaListMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateProviderMangaList>>, TError,{params: UpdateProviderMangaListParams}, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof updateProviderMangaList>>, TError,{params: UpdateProviderMangaListParams}, TContext> => {
const mutationKey = ['updateProviderMangaList'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof updateProviderMangaList>>, {params: UpdateProviderMangaListParams}> = (props) => {
const {params} = props ?? {};
return updateProviderMangaList(params,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type UpdateProviderMangaListMutationResult = NonNullable<Awaited<ReturnType<typeof updateProviderMangaList>>>
export type UpdateProviderMangaListMutationError = unknown
/**
* @summary Queue update provider manga list
*/
export const useUpdateProviderMangaList = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateProviderMangaList>>, TError,{params: UpdateProviderMangaListParams}, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof updateProviderMangaList>>,
TError,
{params: UpdateProviderMangaListParams},
TContext
> => {
const mutationOptions = getUpdateProviderMangaListMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* Queue the retrieval of the manga lists from the content providers
* @summary Queue update manga list
*/
export const updateMangaList = (
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/management/update-manga-list`, method: 'POST', signal
},
options);
}
export const getUpdateMangaListMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMangaList>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof updateMangaList>>, TError,void, TContext> => {
const mutationKey = ['updateMangaList'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof updateMangaList>>, void> = () => {
return updateMangaList(requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type UpdateMangaListMutationResult = NonNullable<Awaited<ReturnType<typeof updateMangaList>>>
export type UpdateMangaListMutationError = unknown
/**
* @summary Queue update manga list
*/
export const useUpdateMangaList = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMangaList>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof updateMangaList>>,
TError,
void,
TContext
> => {
const mutationOptions = getUpdateMangaListMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* Sends a test notification to all users
* @summary Test notification
*/
export const testNotification = (
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/management/test-notification`, method: 'POST', signal
},
options);
}
export const getTestNotificationMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof testNotification>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof testNotification>>, TError,void, TContext> => {
const mutationKey = ['testNotification'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof testNotification>>, void> = () => {
return testNotification(requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type TestNotificationMutationResult = NonNullable<Awaited<ReturnType<typeof testNotification>>>
export type TestNotificationMutationError = unknown
/**
* @summary Test notification
*/
export const useTestNotification = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof testNotification>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof testNotification>>,
TError,
void,
TContext
> => {
const mutationOptions = getTestNotificationMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* Triggers the cleanup of untracked S3 images
* @summary Cleanup unused S3 images
*/
export const imageCleanup = (
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/management/image-cleanup`, method: 'POST', signal
},
options);
}
export const getImageCleanupMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof imageCleanup>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof imageCleanup>>, TError,void, TContext> => {
const mutationKey = ['imageCleanup'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof imageCleanup>>, void> = () => {
return imageCleanup(requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type ImageCleanupMutationResult = NonNullable<Awaited<ReturnType<typeof imageCleanup>>>
export type ImageCleanupMutationError = unknown
/**
* @summary Cleanup unused S3 images
*/
export const useImageCleanup = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof imageCleanup>>, TError,void, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof imageCleanup>>,
TError,
void,
TContext
> => {
const mutationOptions = getImageCleanupMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}

View File

@ -1,5 +1,5 @@
/** /**
* Generated by orval v7.15.0 🍺 * Generated by orval v7.17.0 🍺
* Do not edit manually. * Do not edit manually.
* OpenAPI definition * OpenAPI definition
* OpenAPI spec version: v0 * OpenAPI spec version: v0

View File

@ -1,5 +1,5 @@
/** /**
* Generated by orval v7.15.0 🍺 * Generated by orval v7.17.0 🍺
* Do not edit manually. * Do not edit manually.
* OpenAPI definition * OpenAPI definition
* OpenAPI spec version: v0 * OpenAPI spec version: v0

View File

@ -1,5 +1,5 @@
/** /**
* Generated by orval v7.15.0 🍺 * Generated by orval v7.17.0 🍺
* Do not edit manually. * Do not edit manually.
* OpenAPI definition * OpenAPI definition
* OpenAPI spec version: v0 * OpenAPI spec version: v0
@ -15,10 +15,10 @@ import type {
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import type { import type {
DefaultResponseDTOImportMangaDexResponseDTO, DefaultResponseDTOImportMangaResponseDTO,
DefaultResponseDTOVoid, DefaultResponseDTOVoid,
ImportMangaDexRequestDTO, ImportMultipleFilesBody,
ImportMultipleFilesBody ImportRequestDTO
} from '../api.schemas'; } from '../api.schemas';
import { customInstance } from '../../api'; import { customInstance } from '../../api';
@ -97,30 +97,31 @@ export const useImportMultipleFiles = <TError = unknown,
return useMutation(mutationOptions, queryClient); return useMutation(mutationOptions, queryClient);
} }
/** /**
* Imports manga data from MangaDex into the local database. * Imports manga data from content provider into the local database.
* @summary Import manga from MangaDex * @summary Import manga from content provider
*/ */
export const importFromMangaDex = ( export const importFromProvider = (
importMangaDexRequestDTO: ImportMangaDexRequestDTO, providerId: number,
importRequestDTO: ImportRequestDTO,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => { ) => {
return customInstance<DefaultResponseDTOImportMangaDexResponseDTO>( return customInstance<DefaultResponseDTOImportMangaResponseDTO>(
{url: `/manga/import/manga-dex`, method: 'POST', {url: `/manga/import/provider/${encodeURIComponent(String(providerId))}`, method: 'POST',
headers: {'Content-Type': 'application/json', }, headers: {'Content-Type': 'application/json', },
data: importMangaDexRequestDTO, signal data: importRequestDTO, signal
}, },
options); options);
} }
export const getImportFromMangaDexMutationOptions = <TError = unknown, export const getImportFromProviderMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importFromMangaDex>>, TError,{data: ImportMangaDexRequestDTO}, TContext>, request?: SecondParameter<typeof customInstance>} TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importFromProvider>>, TError,{providerId: number;data: ImportRequestDTO}, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof importFromMangaDex>>, TError,{data: ImportMangaDexRequestDTO}, TContext> => { ): UseMutationOptions<Awaited<ReturnType<typeof importFromProvider>>, TError,{providerId: number;data: ImportRequestDTO}, TContext> => {
const mutationKey = ['importFromMangaDex']; const mutationKey = ['importFromProvider'];
const {mutation: mutationOptions, request: requestOptions} = options ? const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options options
@ -130,10 +131,10 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
const mutationFn: MutationFunction<Awaited<ReturnType<typeof importFromMangaDex>>, {data: ImportMangaDexRequestDTO}> = (props) => { const mutationFn: MutationFunction<Awaited<ReturnType<typeof importFromProvider>>, {providerId: number;data: ImportRequestDTO}> = (props) => {
const {data} = props ?? {}; const {providerId,data} = props ?? {};
return importFromMangaDex(data,requestOptions) return importFromProvider(providerId,data,requestOptions)
} }
@ -141,23 +142,23 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
return { mutationFn, ...mutationOptions }} return { mutationFn, ...mutationOptions }}
export type ImportFromMangaDexMutationResult = NonNullable<Awaited<ReturnType<typeof importFromMangaDex>>> export type ImportFromProviderMutationResult = NonNullable<Awaited<ReturnType<typeof importFromProvider>>>
export type ImportFromMangaDexMutationBody = ImportMangaDexRequestDTO export type ImportFromProviderMutationBody = ImportRequestDTO
export type ImportFromMangaDexMutationError = unknown export type ImportFromProviderMutationError = unknown
/** /**
* @summary Import manga from MangaDex * @summary Import manga from content provider
*/ */
export const useImportFromMangaDex = <TError = unknown, export const useImportFromProvider = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importFromMangaDex>>, TError,{data: ImportMangaDexRequestDTO}, TContext>, request?: SecondParameter<typeof customInstance>} TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importFromProvider>>, TError,{providerId: number;data: ImportRequestDTO}, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult< , queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof importFromMangaDex>>, Awaited<ReturnType<typeof importFromProvider>>,
TError, TError,
{data: ImportMangaDexRequestDTO}, {providerId: number;data: ImportRequestDTO},
TContext TContext
> => { > => {
const mutationOptions = getImportFromMangaDexMutationOptions(options); const mutationOptions = getImportFromProviderMutationOptions(options);
return useMutation(mutationOptions, queryClient); return useMutation(mutationOptions, queryClient);
} }

View File

@ -1,5 +1,5 @@
/** /**
* Generated by orval v7.15.0 🍺 * Generated by orval v7.17.0 🍺
* Do not edit manually. * Do not edit manually.
* OpenAPI definition * OpenAPI definition
* OpenAPI spec version: v0 * OpenAPI spec version: v0
@ -102,6 +102,132 @@ export const useFetchMangaChapters = <TError = unknown,
return useMutation(mutationOptions, queryClient); return useMutation(mutationOptions, queryClient);
} }
/** /**
* Unfollow the manga specified by its ID.
* @summary Unfollow the manga specified by its ID
*/
export const unfollowManga = (
mangaId: number,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/mangas/${encodeURIComponent(String(mangaId))}/unfollowManga`, method: 'POST', signal
},
options);
}
export const getUnfollowMangaMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof unfollowManga>>, TError,{mangaId: number}, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof unfollowManga>>, TError,{mangaId: number}, TContext> => {
const mutationKey = ['unfollowManga'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof unfollowManga>>, {mangaId: number}> = (props) => {
const {mangaId} = props ?? {};
return unfollowManga(mangaId,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type UnfollowMangaMutationResult = NonNullable<Awaited<ReturnType<typeof unfollowManga>>>
export type UnfollowMangaMutationError = unknown
/**
* @summary Unfollow the manga specified by its ID
*/
export const useUnfollowManga = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof unfollowManga>>, TError,{mangaId: number}, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof unfollowManga>>,
TError,
{mangaId: number},
TContext
> => {
const mutationOptions = getUnfollowMangaMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* Follow the manga specified by its ID.
* @summary Follow the manga specified by its ID
*/
export const followManga = (
mangaId: number,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/mangas/${encodeURIComponent(String(mangaId))}/followManga`, method: 'POST', signal
},
options);
}
export const getFollowMangaMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof followManga>>, TError,{mangaId: number}, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof followManga>>, TError,{mangaId: number}, TContext> => {
const mutationKey = ['followManga'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof followManga>>, {mangaId: number}> = (props) => {
const {mangaId} = props ?? {};
return followManga(mangaId,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type FollowMangaMutationResult = NonNullable<Awaited<ReturnType<typeof followManga>>>
export type FollowMangaMutationError = unknown
/**
* @summary Follow the manga specified by its ID
*/
export const useFollowManga = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof followManga>>, TError,{mangaId: number}, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof followManga>>,
TError,
{mangaId: number},
TContext
> => {
const mutationOptions = getFollowMangaMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* Retrieve a list of mangas with their details. * Retrieve a list of mangas with their details.
* @summary Get a list of mangas * @summary Get a list of mangas
*/ */

View File

@ -0,0 +1,127 @@
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import {
useQuery
} from '@tanstack/react-query';
import type {
DataTag,
DefinedInitialDataOptions,
DefinedUseQueryResult,
QueryClient,
QueryFunction,
QueryKey,
UndefinedInitialDataOptions,
UseQueryOptions,
UseQueryResult
} from '@tanstack/react-query';
import type {
DefaultResponseDTOProviderListDTO,
GetProvidersParams
} from '../api.schemas';
import { customInstance } from '../../api';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* Retrieve a list of content providers
* @summary Get a list of providers
*/
export const getProviders = (
params?: GetProvidersParams,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOProviderListDTO>(
{url: `/providers`, method: 'GET',
params, signal
},
options);
}
export const getGetProvidersQueryKey = (params?: GetProvidersParams,) => {
return [
`/providers`, ...(params ? [params]: [])
] as const;
}
export const getGetProvidersQueryOptions = <TData = Awaited<ReturnType<typeof getProviders>>, TError = unknown>(params?: GetProvidersParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getProviders>>, TError, TData>>, request?: SecondParameter<typeof customInstance>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetProvidersQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getProviders>>> = ({ signal }) => getProviders(params, requestOptions, signal);
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getProviders>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetProvidersQueryResult = NonNullable<Awaited<ReturnType<typeof getProviders>>>
export type GetProvidersQueryError = unknown
export function useGetProviders<TData = Awaited<ReturnType<typeof getProviders>>, TError = unknown>(
params: undefined | GetProvidersParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getProviders>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getProviders>>,
TError,
Awaited<ReturnType<typeof getProviders>>
> , 'initialData'
>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetProviders<TData = Awaited<ReturnType<typeof getProviders>>, TError = unknown>(
params?: GetProvidersParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getProviders>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getProviders>>,
TError,
Awaited<ReturnType<typeof getProviders>>
> , 'initialData'
>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetProviders<TData = Awaited<ReturnType<typeof getProviders>>, TError = unknown>(
params?: GetProvidersParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getProviders>>, TError, TData>>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Get a list of providers
*/
export function useGetProviders<TData = Awaited<ReturnType<typeof getProviders>>, TError = unknown>(
params?: GetProvidersParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getProviders>>, TError, TData>>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetProvidersQueryOptions(params,options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}

View File

@ -0,0 +1,188 @@
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View File

@ -10,13 +10,13 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useAuth } from "@/contexts/AuthContext.tsx"; import { useAuth } from "@/contexts/AuthContext.tsx";
import { MangaDexImportDialog } from "@/features/home/components/MangaDexImportDialog.tsx";
import { MangaManualImportDialog } from "@/features/home/components/MangaManualImportDialog.tsx"; import { MangaManualImportDialog } from "@/features/home/components/MangaManualImportDialog.tsx";
import { ProviderImportDialog } from "@/features/home/components/ProviderImportDialog.tsx";
export function ImportDropdown() { export function ImportDropdown() {
const { isAuthenticated } = useAuth(); const { isAuthenticated } = useAuth();
const [mangaDexDialogOpen, setMangaDexDialogOpen] = useState(false); const [providerDialogOpen, setProviderDialogOpen] = useState(false);
const [fileImportDialogOpen, setFileImportDialogOpen] = useState(false); const [fileImportDialogOpen, setFileImportDialogOpen] = useState(false);
if (!isAuthenticated) { if (!isAuthenticated) {
@ -33,9 +33,9 @@ export function ImportDropdown() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className=""> <DropdownMenuContent align="start" className="">
<DropdownMenuItem onClick={() => setMangaDexDialogOpen(true)}> <DropdownMenuItem onClick={() => setProviderDialogOpen(true)}>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
Import from MangaDex Import from Provider
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setFileImportDialogOpen(true)}> <DropdownMenuItem onClick={() => setFileImportDialogOpen(true)}>
<FileUp className="mr-2 h-4 w-4" /> <FileUp className="mr-2 h-4 w-4" />
@ -55,9 +55,9 @@ export function ImportDropdown() {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<MangaDexImportDialog <ProviderImportDialog
mangaDexDialogOpen={mangaDexDialogOpen} dialogOpen={providerDialogOpen}
onMangaDexDialogOpenChange={setMangaDexDialogOpen} onDialogOpenChange={setProviderDialogOpen}
/> />
<MangaManualImportDialog <MangaManualImportDialog

View File

@ -1,134 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { useImportFromMangaDex } from "@/api/generated/manga-import/manga-import.ts";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog.tsx";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form.tsx";
import { Input } from "@/components/ui/input";
interface MangaDexImportDialogProps {
mangaDexDialogOpen: boolean;
onMangaDexDialogOpenChange: (open: boolean) => void;
}
export const MangaDexImportDialog = ({
mangaDexDialogOpen,
onMangaDexDialogOpenChange,
}: MangaDexImportDialogProps) => {
const formSchema = z
.object({
value: z.string().min(1, "Please enter a MangaDex ID or URL."),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
value: "",
},
});
const { mutate: importMangaDex, isPending: isPendingImportMangaDex } =
useImportFromMangaDex({
mutation: {
onSuccess: () => {
form.reset();
onMangaDexDialogOpenChange(false);
toast.success("Manga imported successfully!");
},
},
});
const handleSubmit = useCallback(
(data: z.infer<typeof formSchema>) => {
let id = data.value;
if (data.value.length > 36) {
const match = data.value.match(/title\/([0-9a-fA-F-]{36})/);
if (match) {
id = match[1];
} else {
alert("Invalid MangaDex URL or ID");
return;
}
}
if (id.length !== 36) {
alert("Invalid MangaDex ID");
return;
}
importMangaDex({ data: { id } });
},
[formSchema, importMangaDex],
);
return (
<Dialog open={mangaDexDialogOpen} onOpenChange={onMangaDexDialogOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Import from MangaDex</DialogTitle>
<DialogDescription>
Enter a MangaDex manga URL or ID to import it to your library.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
id="importForm"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="value"
render={({ field }) => (
<FormItem>
<FormLabel>MangaDex URL or ID</FormLabel>
<FormControl>
<Input
placeholder="e.g., https://mangadex.org/title/..."
disabled={isPendingImportMangaDex}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onMangaDexDialogOpenChange(false)}
>
Cancel
</Button>
<Button
type="submit"
disabled={isPendingImportMangaDex}
form="importForm"
>
Import
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,190 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { useImportFromProvider } from "@/api/generated/manga-import/manga-import.ts";
import { useGetProviders } from "@/api/generated/provider/provider.ts";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog.tsx";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form.tsx";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select.tsx";
interface ProviderImportDialogProps {
dialogOpen: boolean;
onDialogOpenChange: (open: boolean) => void;
}
export const ProviderImportDialog = ({
dialogOpen,
onDialogOpenChange,
}: ProviderImportDialogProps) => {
const formSchema = z.object({
value: z.string().min(1, "Please enter an ID or URL."),
providerId: z.string(),
myAnimeListId: z.string().optional(),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
value: "",
providerId: "",
myAnimeListId: undefined,
},
});
const { data: providerData, isFetching: isFetchingProviders } =
useGetProviders({ manualImport: true });
const { mutate: importFromProvider, isPending: isPendingImportFromProvider } =
useImportFromProvider({
mutation: {
onSuccess: () => {
form.reset();
onDialogOpenChange(false);
toast.success("Manga imported successfully!");
},
},
});
const handleSubmit = useCallback(
(data: z.output<typeof formSchema>) => {
importFromProvider({
providerId: Number(data.providerId),
data: { id: data.value, metadataId: data.myAnimeListId },
});
},
[importFromProvider],
);
return (
<Dialog open={dialogOpen} onOpenChange={onDialogOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Import from Provider</DialogTitle>
<DialogDescription>
Enter a Provider manga URL or ID to import it to your library.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
id="importForm"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="providerId"
render={({ field }) => (
<FormItem>
<FormLabel>Provider</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
{...field}
disabled={isFetchingProviders}
required
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a provider" />
</SelectTrigger>
<SelectContent>
{providerData?.data?.providers?.map((provider) => (
<SelectItem
key={provider.id}
value={provider.id.toString()}
>
{provider.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="value"
render={({ field }) => (
<FormItem>
<FormLabel>URL or ID</FormLabel>
<FormControl>
<Input
placeholder="e.g., https://mangadex.org/title/..."
disabled={isPendingImportFromProvider}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="myAnimeListId"
render={({ field }) => (
<FormItem>
<FormLabel>MyAnimeList ID (Optional)</FormLabel>
<FormControl>
<Input
placeholder="e.g., 13 (for One Piece)"
disabled={isPendingImportFromProvider}
{...field}
/>
</FormControl>
<FormDescription>
Optionally link this manga to a MyAnimeList entry for better
precision on metadata fetching.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onDialogOpenChange(false)}
>
Cancel
</Button>
<Button
type="submit"
disabled={isPendingImportFromProvider || isFetchingProviders}
form="importForm"
>
Import
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -1,5 +1,5 @@
import { ArrowLeft, ChevronLeft, ChevronRight, Home } from "lucide-react"; import { ArrowLeft, ChevronLeft, ChevronRight, Home } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router"; import { useNavigate, useParams } from "react-router";
import { import {
useGetMangaChapterImages, useGetMangaChapterImages,
@ -21,34 +21,95 @@ const Chapter = () => {
getCurrentChapterPage(chapterNumber) ?? 1, getCurrentChapterPage(chapterNumber) ?? 1,
); );
const { data, isLoading } = useGetMangaChapterImages(chapterNumber); const [infiniteScroll, setInfiniteScroll] = useState(true);
const { data, isLoading } = useGetMangaChapterImages(chapterNumber);
const { mutate } = useMarkAsRead(); const { mutate } = useMarkAsRead();
// For infinite scroll mode
const [visibleCount, setVisibleCount] = useState(1);
const loadMoreRef = useRef(null);
/** Mark chapter as read when last page reached */
useEffect(() => { useEffect(() => {
if (!data || isLoading) { if (!data || isLoading) return;
return;
}
if (currentPage === data.data?.chapterImageKeys.length) { if (currentPage === data.data?.chapterImageKeys.length) {
mutate({ chapterId: chapterNumber }); mutate({ chapterId: chapterNumber });
} }
}, [data, mutate, currentPage]); }, [data, mutate, currentPage]);
/** Persist reading progress */
useEffect(() => { useEffect(() => {
setCurrentChapterPage(chapterNumber, currentPage); setCurrentChapterPage(chapterNumber, currentPage);
}, [chapterNumber, currentPage]); }, [chapterNumber, currentPage]);
/** Restore stored page */
useEffect(() => { useEffect(() => {
if (!isLoading && !data?.data) { if (!isLoading && data?.data) {
return; const stored = getCurrentChapterPage(chapterNumber);
if (stored) {
setCurrentPage(stored);
setVisibleCount(stored); // for infinite scroll
}
} }
}, [isLoading, data?.data]);
const storedChapterPage = getCurrentChapterPage(chapterNumber); /** Infinite scroll observer */
if (storedChapterPage) { useEffect(() => {
setCurrentPage(storedChapterPage); if (!infiniteScroll) return;
if (!loadMoreRef.current) return;
const obs = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
setVisibleCount((count) =>
Math.min(count + 2, data?.data?.chapterImageKeys.length ?? 0),
);
}
});
obs.observe(loadMoreRef.current);
return () => obs.disconnect();
}, [infiniteScroll, data?.data]);
/** Track which image is currently visible (for progress update) */
useEffect(() => {
if (!infiniteScroll) return;
const imgs = document.querySelectorAll("[data-page]");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const el = entry.target as HTMLElement; // <-- FIX
if (entry.isIntersecting) {
const pageNum = Number(el.dataset.page); // <-- SAFE
setCurrentPage(pageNum);
}
});
},
{ threshold: 0.5 },
);
imgs.forEach((img) => observer.observe(img));
return () => observer.disconnect();
}, [infiniteScroll, visibleCount]);
useEffect(() => {
if (!data?.data) return;
// When switching modes:
if (infiniteScroll) {
// Scroll mode → show saved progress
setVisibleCount(currentPage);
setTimeout(() => {
const el = document.querySelector(`[data-page="${currentPage}"]`);
el?.scrollIntoView({ behavior: "smooth", block: "start" });
}, 50);
} else {
// Single page mode → scroll to top
window.scrollTo({ top: 0 });
} }
}, [getCurrentChapterPage, isLoading, data?.data]); }, [infiniteScroll]);
if (!data?.data) { if (!data?.data) {
return ( return (
@ -65,27 +126,25 @@ const Chapter = () => {
); );
} }
const goToNextPage = () => { const images = data.data.chapterImageKeys;
if (!data?.data) {
return;
}
if (currentPage < data.data.chapterImageKeys.length) { /** Standard navigation (non-infinite mode) */
setCurrentPage(currentPage + 1); const goToNextPage = () => {
if (currentPage < images.length) {
setCurrentPage((p) => p + 1);
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
} }
}; };
const goToPreviousPage = () => { const goToPreviousPage = () => {
if (currentPage > 1) { if (currentPage > 1) {
setCurrentPage(currentPage - 1); setCurrentPage((p) => p - 1);
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
} }
}; };
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
{/* Header */} {/* HEADER */}
<header className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <header className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="px-4 py-4 sm:px-8"> <div className="px-4 py-4 sm:px-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -99,6 +158,7 @@ const Chapter = () => {
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
<span className="hidden sm:inline">Back</span> <span className="hidden sm:inline">Back</span>
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@ -108,6 +168,7 @@ const Chapter = () => {
<Home className="h-4 w-4" /> <Home className="h-4 w-4" />
<span className="hidden sm:inline">Home</span> <span className="hidden sm:inline">Home</span>
</Button> </Button>
<div className="hidden sm:block"> <div className="hidden sm:block">
<h1 className="text-sm font-semibold text-foreground"> <h1 className="text-sm font-semibold text-foreground">
{data.data.mangaTitle} {data.data.mangaTitle}
@ -117,19 +178,27 @@ const Chapter = () => {
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Page {currentPage} / {data.data.chapterImageKeys.length} Page {currentPage} / {images.length}
</span> </span>
<Button
variant="outline"
size="sm"
onClick={() => setInfiniteScroll((v) => !v)}
className="text-xs"
>
{infiniteScroll ? "Single Page Mode" : "Scroll Mode"}
</Button>
<ThemeToggle /> <ThemeToggle />
</div> </div>
</div> </div>
</div> </div>
</header> </header>
{/* Reader Content */} {/* MAIN */}
<main className="mx-auto max-w-4xl px-4 py-8"> <main className="mx-auto max-w-4xl px-4 py-8">
{/* Mobile title */}
<div className="mb-4 sm:hidden"> <div className="mb-4 sm:hidden">
<h1 className="text-lg font-semibold text-foreground"> <h1 className="text-lg font-semibold text-foreground">
{data.data.mangaTitle} {data.data.mangaTitle}
@ -139,74 +208,74 @@ const Chapter = () => {
</p> </p>
</div> </div>
{/* Manga Page */} {/* ------------------------------------------------------------------ */}
<div className="relative mx-auto mb-8 overflow-hidden rounded-lg border border-border bg-muted"> {/* MODE 1 --- INFINITE SCROLL MODE */}
<img {/* ------------------------------------------------------------------ */}
src={ {infiniteScroll ? (
import.meta.env.VITE_OMV_BASE_URL + <div className="flex flex-col space-y-0">
"/" + {images.slice(0, visibleCount).map((key, idx) => (
data.data.chapterImageKeys[currentPage - 1] || <img
"/placeholder.svg" key={idx}
} data-page={idx + 1}
alt={`Page ${currentPage}`} src={`${import.meta.env.VITE_OMV_BASE_URL}/${key}`}
width={1000} className="w-full h-auto block"
height={1400} alt={`Page ${idx + 1}`}
className="h-auto w-full" loading="lazy"
// priority />
/> ))}
</div>
{/* Navigation Controls */} {/* LOAD MORE SENTINEL */}
<div className="space-y-4"> <div ref={loadMoreRef} className="h-10" />
{/* Page Navigation */}
<div className="flex items-center justify-center gap-4">
<Button
onClick={goToPreviousPage}
disabled={currentPage === 1}
variant="outline"
className="gap-2 bg-transparent"
>
<ChevronLeft className="h-4 w-4" />
Previous Page
</Button>
<span className="text-sm font-medium text-foreground">
{currentPage} / {data.data.chapterImageKeys.length}
</span>
<Button
onClick={goToNextPage}
disabled={currentPage === data.data.chapterImageKeys.length}
variant="outline"
className="gap-2 bg-transparent"
>
Next Page
<ChevronRight className="h-4 w-4" />
</Button>
</div> </div>
) : (
/* ------------------------------------------------------------------ */
/* MODE 2 --- STANDARD SINGLE-PAGE MODE */
/* ------------------------------------------------------------------ */
<>
<div className="relative mx-auto mb-8 overflow-hidden rounded-lg border border-border bg-muted">
<img
src={
import.meta.env.VITE_OMV_BASE_URL +
"/" +
images[currentPage - 1] || "/placeholder.svg"
}
alt={`Page ${currentPage}`}
width={1000}
height={1400}
className="h-auto w-full"
/>
</div>
{/*/!* Chapter Navigation *!/*/} {/* NAVIGATION BUTTONS */}
{/*{(currentPage === data.chapterImageKeys.length || currentPage === 1) && (*/} <div className="space-y-4">
{/* <div className="flex items-center justify-center gap-4 border-t border-border pt-4">*/} <div className="flex items-center justify-center gap-4">
{/* <Button*/} <Button
{/* onClick={goToPreviousChapter}*/} onClick={goToPreviousPage}
{/* disabled={chapterNumber === 1}*/} disabled={currentPage === 1}
{/* variant="secondary"*/} variant="outline"
{/* className="gap-2"*/} className="gap-2 bg-transparent"
{/* >*/} >
{/* <ChevronLeft className="h-4 w-4" />*/} <ChevronLeft className="h-4 w-4" />
{/* Previous Chapter*/} Previous Page
{/* </Button>*/} </Button>
{/* <Button*/}
{/* onClick={goToNextChapter}*/} <span className="text-sm font-medium text-foreground">
{/* disabled={chapterNumber === manga.chapters}*/} {currentPage} / {images.length}
{/* variant="secondary"*/} </span>
{/* className="gap-2"*/}
{/* >*/} <Button
{/* Next Chapter*/} onClick={goToNextPage}
{/* <ChevronRight className="h-4 w-4" />*/} disabled={currentPage === images.length}
{/* </Button>*/} variant="outline"
{/* </div>*/} className="gap-2 bg-transparent"
{/*)}*/} >
</div> Next Page
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</>
)}
</main> </main>
</div> </div>
); );

View File

@ -1,18 +1,27 @@
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { import {
ArrowLeft, ArrowLeft,
Bell,
BellOff,
BookOpen, BookOpen,
Calendar, Calendar,
ChevronDown, ChevronDown,
Database, Database,
Heart,
Star, Star,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useCallback, useState } from "react";
import { useNavigate, useParams } from "react-router"; import { useNavigate, useParams } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import {
useSetFavorite,
useSetUnfavorite,
} from "@/api/generated/favorite-mangas/favorite-mangas.ts";
import { import {
useFetchMangaChapters, useFetchMangaChapters,
useFollowManga,
useGetManga, useGetManga,
useUnfollowManga,
} from "@/api/generated/manga/manga.ts"; } from "@/api/generated/manga/manga.ts";
import { useFetchAllChapters } from "@/api/generated/manga-chapter/manga-chapter.ts"; import { useFetchAllChapters } from "@/api/generated/manga-chapter/manga-chapter.ts";
import { ThemeToggle } from "@/components/ThemeToggle.tsx"; import { ThemeToggle } from "@/components/ThemeToggle.tsx";
@ -24,10 +33,12 @@ import {
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible.tsx"; } from "@/components/ui/collapsible.tsx";
import { useAuth } from "@/contexts/AuthContext.tsx";
import { MangaChapter } from "@/features/manga/MangaChapter.tsx"; import { MangaChapter } from "@/features/manga/MangaChapter.tsx";
import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts"; import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts";
const Manga = () => { const Manga = () => {
const { isAuthenticated } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const params = useParams(); const params = useParams();
const mangaId = Number(params.mangaId); const mangaId = Number(params.mangaId);
@ -53,6 +64,63 @@ const Manga = () => {
const [openProviders, setOpenProviders] = useState<Set<number>>(new Set()); const [openProviders, setOpenProviders] = useState<Set<number>>(new Set());
const { mutate: mutateFavorite, isPending: isPendingFavorite } =
useSetFavorite({
mutation: {
onSuccess: () => queryClient.invalidateQueries({ queryKey }),
},
});
const { mutate: mutateUnfavorite, isPending: isPendingUnfavorite } =
useSetUnfavorite({
mutation: {
onSuccess: () => queryClient.invalidateQueries({ queryKey }),
},
});
const isPendingFavoriteChange = isPendingFavorite || isPendingUnfavorite;
const handleFavoriteClick = useCallback(
(isFavorite: boolean) =>
isFavorite
? mutateUnfavorite({ id: mangaData?.data?.id ?? -1 })
: mutateFavorite({ id: mangaData?.data?.id ?? -1 }),
[mutateUnfavorite, mutateFavorite, mangaData?.data?.id],
);
const { mutate: mutateFollow, isPending: isPendingFollow } = useFollowManga({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey });
toast.success(
"We will notify you when new content if available for this manga.",
);
},
},
});
const { mutate: mutateUnfollow, isPending: isPendingUnfollow } =
useUnfollowManga({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey });
toast.success(
"You will no longer received notifications for this manga.",
);
},
},
});
const isPendingFollowChange = isPendingFollow || isPendingUnfollow;
const handleFollowClick = useCallback(
(isFollowing: boolean) =>
isFollowing
? mutateUnfollow({ mangaId: mangaData?.data?.id ?? -1 })
: mutateFollow({ mangaId: mangaData?.data?.id ?? -1 }),
[mangaData?.data?.id, mutateUnfollow, mutateFollow],
);
if (!mangaData) { if (!mangaData) {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-background"> <div className="flex min-h-screen items-center justify-center bg-background">
@ -125,16 +193,57 @@ const Manga = () => {
{/* Details */} {/* Details */}
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<div className="mb-3 flex items-start justify-between gap-4"> <div className="mb-3 flex items-center justify-between gap-4">
<h1 className="text-balance text-4xl font-bold tracking-tight text-foreground"> <h1 className="text-balance text-4xl font-bold tracking-tight text-foreground">
{mangaData.data?.title} {mangaData.data?.title}
</h1> </h1>
<Badge <div className="flex gap-4 items-center">
variant="secondary" <Badge
className="border border-border bg-card text-foreground" variant="secondary"
> className="border border-border bg-card text-foreground max-h-6"
{mangaData.data?.status} >
</Badge> {mangaData.data?.status}
</Badge>
{isAuthenticated && (
<>
<Button
size="icon"
variant="outline"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleFollowClick(
mangaData?.data?.following || false,
);
}}
disabled={isPendingFollowChange}
>
{mangaData?.data?.following ? <BellOff /> : <Bell />}
</Button>
<Button
size="icon"
variant="outline"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleFavoriteClick(
mangaData?.data?.favorite || false,
);
}}
disabled={isPendingFavoriteChange}
>
<Heart
className={`h-4 w-4 transition-colors ${
mangaData?.data?.favorite
? "fill-red-500 text-red-500"
: "text-muted-foreground hover:text-red-500"
}`}
/>
</Button>
</>
)}
</div>
</div> </div>
<p className="text-lg text-muted-foreground"> <p className="text-lg text-muted-foreground">
{mangaData.data?.authors.join(", ")} {mangaData.data?.authors.join(", ")}
@ -236,6 +345,13 @@ const Manga = () => {
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
{mangaData.data?.providers.length === 0 && (
<div className="flex justify-center">
<p className="text-foreground">
No providers available for this manga.
</p>
</div>
)}
{mangaData.data?.providers.map((provider) => ( {mangaData.data?.providers.map((provider) => (
<Card key={provider.id} className="border-border bg-card"> <Card key={provider.id} className="border-border bg-card">
<Collapsible <Collapsible
@ -257,36 +373,37 @@ const Manga = () => {
</p> </p>
</div> </div>
</div> </div>
{provider.supportsChapterFetch && ( {provider.supportsChapterFetch &&
<div className={"flex gap-4 pr-4"}> provider.providerStatus === "ACTIVE" && (
<Button <div className={"flex gap-4 pr-4"}>
size="sm" <Button
variant="outline" size="sm"
disabled={isPending} variant="outline"
onClick={() => disabled={isPending}
fetchAllMutate({ onClick={() =>
mangaProviderId: provider.id, fetchAllMutate({
}) mangaProviderId: provider.id,
} })
className="gap-2" }
> className="gap-2"
<Database className="h-4 w-4" /> >
Fetch all from Provider <Database className="h-4 w-4" />
</Button> Fetch all from Provider
<Button </Button>
size="sm" <Button
variant="outline" size="sm"
disabled={isPending} variant="outline"
onClick={() => disabled={isPending}
mutate({ mangaProviderId: provider.id }) onClick={() =>
} mutate({ mangaProviderId: provider.id })
className="gap-2" }
> className="gap-2"
<Database className="h-4 w-4" /> >
Fetch from Provider <Database className="h-4 w-4" />
</Button> Fetch from Provider
</div> </Button>
)} </div>
)}
</div> </div>
<ChevronDown <ChevronDown
className={`h-5 w-5 text-muted-foreground transition-transform ${ className={`h-5 w-5 text-muted-foreground transition-transform ${