diff --git a/.env b/.env index e6e1599..52ab6b9 100644 --- a/.env +++ b/.env @@ -27,3 +27,8 @@ POSTGRES_PORT=5432 ###> nelmio/cors-bundle ### CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' ###< nelmio/cors-bundle ### + +MANGADEX_CLIENT_ID='personal-client-c6ea0ee7-8d48-41cd-8813-51b874177332-627526e7' +MANGADEX_CLIENT_SECRET='abMpCrSDYMWPjd24Pitl14t6RFqTs0cy' +MANGADEX_USERNAME='Colgora' +MANGADEX_PASSWORD='Hagaren666!' diff --git a/Dockerfile b/Dockerfile index 8f0dccb..ac8b458 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,18 @@ RUN apk add --no-cache \ ; # Install Node.js and npm -RUN apk add --no-cache nodejs npm +ENV CHROME_BIN="/usr/bin/chromium-browser" \ + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD="true" +RUN set -x \ + && apk update \ + && apk upgrade \ + && apk add --no-cache \ + nodejs \ + npm \ + udev \ + ttf-freefont \ + chromium \ + && npm install puppeteer@1.10.0 RUN set -eux; \ install-php-extensions \ diff --git a/Makefile b/Makefile index 4bd11f9..9984cc3 100644 --- a/Makefile +++ b/Makefile @@ -126,6 +126,15 @@ state-processor: ## Create a new state processor state-provider: ## Create a new state provider @$(SYMFONY) make:state-provider +twig-component: ## Create a new twig component + @$(SYMFONY) make:twig-component + +twig-extension: ## Create a new twig extension + @$(SYMFONY) make:twig-extension + +stimulus: ## Create a new stimulus controller + @$(SYMFONY) make:stimulus-controller + ## —— Webpack Encore ————————————————————————————————————————————————————————————— npm-install: ## Install npm dependencies @$(DOCKER_COMP) exec php npm install diff --git a/assets/app.js b/assets/app.js index 2b5d790..e66af97 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1,3 +1,6 @@ +import './bootstrap.js'; + +import '@fortawesome/fontawesome-free/js/all.js'; /* * Welcome to your app's main JavaScript file! * diff --git a/assets/controllers.json b/assets/controllers.json index a1c6e90..b980571 100644 --- a/assets/controllers.json +++ b/assets/controllers.json @@ -1,4 +1,14 @@ { - "controllers": [], + "controllers": { + "@symfony/ux-live-component": { + "live": { + "enabled": true, + "fetch": "eager", + "autoimport": { + "@symfony/ux-live-component/dist/live.min.css": true + } + } + } + }, "entrypoints": [] } diff --git a/assets/controllers/bootstrap-modal_controller.js b/assets/controllers/bootstrap-modal_controller.js new file mode 100644 index 0000000..8967945 --- /dev/null +++ b/assets/controllers/bootstrap-modal_controller.js @@ -0,0 +1,21 @@ +import { Controller } from '@hotwired/stimulus'; +import { Modal } from 'bootstrap'; + +/** + * Allows you to dispatch a "modal:close" JavaScript event to close it. + * + * This is useful inside a LiveComponent, where you can emit a browser event + * to open or close the modal. + * + * See templates/components/BootstrapModal.html.twig to see how this is + * attached to Bootstrap modal. + */ +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + modal = null; + + connect() { + this.modal = Modal.getOrCreateInstance(this.element); + document.addEventListener('modal:close', () => this.modal.hide()); + } +} diff --git a/assets/controllers/search_controller.js b/assets/controllers/search_controller.js new file mode 100644 index 0000000..e29e332 --- /dev/null +++ b/assets/controllers/search_controller.js @@ -0,0 +1,15 @@ +import { Controller } from '@hotwired/stimulus'; + +/* +* The following line makes this controller "lazy": it won't be downloaded until needed +* See https://github.com/symfony/stimulus-bridge#lazy-controllers +*/ +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static targets = ['input'] + + clearSearch() { + this.inputTarget.value = ''; + this.inputTarget.focus(); + } +} diff --git a/assets/controllers/table_controller.js b/assets/controllers/table_controller.js new file mode 100644 index 0000000..ae125bc --- /dev/null +++ b/assets/controllers/table_controller.js @@ -0,0 +1,24 @@ +import {Controller} from '@hotwired/stimulus'; + +/* +* The following line makes this controller "lazy": it won't be downloaded until needed +* See https://github.com/symfony/stimulus-bridge#lazy-controllers +*/ +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static targets = ['body'] + + // ... + collapse(event) { + if (this.bodyTarget.style.display === "none") { + this.bodyTarget.style.display = "block"; + event.currentTarget.classList.remove('fa-chevron-up'); + event.currentTarget.classList.add('fa-chevron-down'); + } else { + this.bodyTarget.style.display = "none"; + event.currentTarget.classList.remove('fa-chevron-down'); + event.currentTarget.classList.add('fa-chevron-up'); + } + + } +} diff --git a/assets/styles/app.scss b/assets/styles/app.scss index 9b49d48..8eba4cb 100644 --- a/assets/styles/app.scss +++ b/assets/styles/app.scss @@ -1,3 +1,4 @@ +//@import "bootstrap/scss/bootstrap"; @import "tailwindcss/base"; @import "tailwindcss/components"; @import "tailwindcss/utilities"; @@ -6,3 +7,94 @@ body { background-color: white; } +.modal { + z-index: 1072!important; + @apply hidden fixed top-0 left-0 w-full h-full outline-none +} + +.modal-dialog { + z-index: 1073!important; +} + +.modal.show { + @apply block +} + +.modal-backdrop { + z-index: 9!important; + width: 100vw; + height: 100vh; + @apply fixed bg-black top-0 left-0 +} + +.modal-backdrop.fade { + @apply opacity-0 +} + +.modal-backdrop.show { + @apply opacity-50 +} + +.modal.fade .modal-dialog { + transition: -webkit-transform .3s ease-out; + transition: transform .3s ease-out; + transition: transform .3s ease-out, -webkit-transform .3s ease-out; + -webkit-transform: translate(0, -50px); + transform: translate(0, -50px); +} + +.modal.show .modal-dialog { + -webkit-transform: none; + transform: none; +} + +::-webkit-scrollbar { + @apply w-2 h-1; + /* Ajuster la largeur et la hauteur de la scrollbar */ +} + +::-webkit-scrollbar-thumb { + @apply bg-green-600; +} + +::-webkit-scrollbar-thumb:hover { + @apply bg-green-700; +} + +::-webkit-scrollbar-track { + @apply bg-white; +} + +#searchResults::-webkit-scrollbar { + @apply w-2 h-1; + /* Ajuster la largeur et la hauteur de la scrollbar */ +} + +#searchResults::-webkit-scrollbar-thumb { + @apply bg-green-600 rounded-r-sm; +} + +#searchResults::-webkit-scrollbar-thumb:hover { + @apply bg-green-700; +} + +#searchResults::-webkit-scrollbar-track { + @apply bg-gray-700; +} + +///* Custom styles for the scrollbar buttons */ +//::-webkit-scrollbar-button { +// @apply bg-gray-700; +// height: 10px; /* Adjust the height of the scrollbar buttons */ +// width: 10px; /* Adjust the width of the scrollbar buttons */ +//} +// +//::-webkit-scrollbar-button:vertical:decrement { +// @apply bg-gray-700; +// background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M12 8l6 6H6z'/%3E%3C/svg%3E"); +//} +// +//::-webkit-scrollbar-button:vertical:increment { +// @apply bg-gray-700; +// background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M12 16l-6-6h12z'/%3E%3C/svg%3E"); +//} diff --git a/compose.override.yaml b/compose.override.yaml index 177a547..1fe84cd 100644 --- a/compose.override.yaml +++ b/compose.override.yaml @@ -20,9 +20,6 @@ services: - host.docker.internal:host-gateway tty: true -###> symfony/mercure-bundle ### -###< symfony/mercure-bundle ### - ###> doctrine/doctrine-bundle ### database: ports: diff --git a/compose.yaml b/compose.yaml index 1b668a2..1bb6c99 100644 --- a/compose.yaml +++ b/compose.yaml @@ -20,6 +20,8 @@ services: volumes: - caddy_data:/data - caddy_config:/config + networks: + - mangarr_network ports: # HTTP - target: 80 @@ -35,11 +37,11 @@ services: protocol: udp # Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service -###> symfony/mercure-bundle ### -###< symfony/mercure-bundle ### ###> doctrine/doctrine-bundle ### database: + hostname: database + container_name: database image: postgres:${POSTGRES_VERSION:-16}-alpine environment: POSTGRES_DB: ${POSTGRES_DB:-app} @@ -48,6 +50,8 @@ services: POSTGRES_USER: ${POSTGRES_USER:-app} volumes: - database_data:/var/lib/postgresql/data:rw + networks: + - mangarr_network ports: - '5432:5432' # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data! @@ -63,9 +67,10 @@ services: volumes: caddy_data: caddy_config: -###> symfony/mercure-bundle ### -###< symfony/mercure-bundle ### ###> doctrine/doctrine-bundle ### database_data: ###< doctrine/doctrine-bundle ### +networks: + mangarr_network: + external: true diff --git a/composer.json b/composer.json index ec820c3..71b31f9 100644 --- a/composer.json +++ b/composer.json @@ -27,16 +27,21 @@ "symfony/flex": "^2", "symfony/framework-bundle": "7.0.*", "symfony/http-client": "7.0.*", + "symfony/mime": "7.0.*", "symfony/monolog-bundle": "^3.10", "symfony/property-access": "7.0.*", "symfony/property-info": "7.0.*", "symfony/runtime": "7.0.*", "symfony/security-bundle": "7.0.*", "symfony/serializer": "7.0.*", + "symfony/stimulus-bundle": "^2.17", "symfony/twig-bundle": "7.0.*", + "symfony/ux-live-component": "^2.17", "symfony/validator": "7.0.*", "symfony/webpack-encore-bundle": "^2.1", - "symfony/yaml": "7.0.*" + "symfony/yaml": "7.0.*", + "twig/extra-bundle": "^2.12|^3.0", + "twig/twig": "^2.12|^3.0" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index 8158457..1e17a2f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "721e53091aa5df1279e1b346d6a8e9b7", + "content-hash": "08c76de0049c9ace64fab221e185979c", "packages": [ { "name": "api-platform/core", @@ -4561,6 +4561,90 @@ ], "time": "2023-12-30T15:41:17+00:00" }, + { + "name": "symfony/mime", + "version": "v7.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "3426d1e95f432c82ceef57e9943383116800f406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/3426d1e95f432c82ceef57e9943383116800f406", + "reference": "3426d1e95f432c82ceef57e9943383116800f406", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-06-02T15:49:03+00:00" + }, { "name": "symfony/monolog-bridge", "version": "v7.0.3", @@ -4873,6 +4957,90 @@ ], "time": "2023-01-26T09:26:14+00:00" }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "a287ed7475f85bf6f61890146edbc932c0fff919" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919", + "reference": "a287ed7475f85bf6f61890146edbc932c0fff919", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, { "name": "symfony/polyfill-intl-normalizer", "version": "v1.28.0", @@ -5966,6 +6134,75 @@ ], "time": "2023-12-26T14:02:43+00:00" }, + { + "name": "symfony/stimulus-bundle", + "version": "v2.17.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stimulus-bundle.git", + "reference": "b828a32fe9f75500d26b563cc01874657162c413" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/b828a32fe9f75500d26b563cc01874657162c413", + "reference": "b828a32fe9f75500d26b563cc01874657162c413", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.0|^3.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "twig/twig": "^2.15.3|^3.8" + }, + "require-dev": { + "symfony/asset-mapper": "^6.3|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "zenstruck/browser": "^1.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\UX\\StimulusBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration with your Symfony app & Stimulus!", + "keywords": [ + "symfony-ux" + ], + "support": { + "source": "https://github.com/symfony/stimulus-bundle/tree/v2.17.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-21T10:23:35+00:00" + }, { "name": "symfony/stopwatch", "version": "v7.0.0", @@ -6384,6 +6621,182 @@ ], "time": "2024-01-23T15:02:46+00:00" }, + { + "name": "symfony/ux-live-component", + "version": "v2.17.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ux-live-component.git", + "reference": "65947f886b3835a504dd86951b5d07ccc4dcb5e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/65947f886b3835a504dd86951b5d07ccc4dcb5e1", + "reference": "65947f886b3835a504dd86951b5d07ccc4dcb5e1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/property-access": "^5.4.5|^6.0|^7.0", + "symfony/ux-twig-component": "^2.8", + "twig/twig": "^3.8.0" + }, + "conflict": { + "symfony/config": "<5.4.0" + }, + "require-dev": { + "doctrine/annotations": "^1.0", + "doctrine/collections": "^1.6.8|^2.0", + "doctrine/doctrine-bundle": "^2.4.3", + "doctrine/orm": "^2.9.4", + "doctrine/persistence": "^2.5.2|^3.0", + "phpdocumentor/reflection-docblock": "5.x-dev", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/options-resolver": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^6.1|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/security-bundle": "^5.4|^6.0|^7.0", + "symfony/serializer": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/validator": "^5.4|^6.0|^7.0", + "zenstruck/browser": "^1.2.0", + "zenstruck/foundry": "1.37.*" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "name": "symfony/ux", + "url": "https://github.com/symfony/ux" + } + }, + "autoload": { + "psr-4": { + "Symfony\\UX\\LiveComponent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Live components for Symfony", + "homepage": "https://symfony.com", + "keywords": [ + "components", + "symfony-ux", + "twig" + ], + "support": { + "source": "https://github.com/symfony/ux-live-component/tree/v2.17.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-22T18:53:03+00:00" + }, + { + "name": "symfony/ux-twig-component", + "version": "v2.17.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ux-twig-component.git", + "reference": "fb3d978b7f19e9a94533a3bf30d68269908ffae1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/fb3d978b7f19e9a94533a3bf30d68269908ffae1", + "reference": "fb3d978b7f19e9a94533a3bf30d68269908ffae1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "twig/twig": "^3.8" + }, + "conflict": { + "symfony/config": "<5.4.0" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^6.0|^7.0", + "symfony/stimulus-bundle": "^2.9.1", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/webpack-encore-bundle": "^1.15" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "name": "symfony/ux", + "url": "https://github.com/symfony/ux" + } + }, + "autoload": { + "psr-4": { + "Symfony\\UX\\TwigComponent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Twig components for Symfony", + "homepage": "https://symfony.com", + "keywords": [ + "components", + "symfony-ux", + "twig" + ], + "support": { + "source": "https://github.com/symfony/ux-twig-component/tree/v2.17.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-19T16:14:05+00:00" + }, { "name": "symfony/validator", "version": "v7.0.3", @@ -6860,6 +7273,80 @@ ], "time": "2023-11-07T10:26:03+00:00" }, + { + "name": "twig/extra-bundle", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/twig-extra-bundle.git", + "reference": "cdc6e23aeb7f4953c1039568c3439aab60c56454" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/cdc6e23aeb7f4953c1039568c3439aab60c56454", + "reference": "cdc6e23aeb7f4953c1039568c3439aab60c56454", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/framework-bundle": "^5.4|^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0", + "twig/twig": "^3.0" + }, + "require-dev": { + "league/commonmark": "^1.0|^2.0", + "symfony/phpunit-bridge": "^6.4|^7.0", + "twig/cache-extra": "^3.0", + "twig/cssinliner-extra": "^3.0", + "twig/html-extra": "^3.0", + "twig/inky-extra": "^3.0", + "twig/intl-extra": "^3.0", + "twig/markdown-extra": "^3.0", + "twig/string-extra": "^3.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Twig\\Extra\\TwigExtraBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Symfony bundle for extra Twig extensions", + "homepage": "https://twig.symfony.com", + "keywords": [ + "bundle", + "extra", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2024-05-11T07:35:57+00:00" + }, { "name": "twig/twig", "version": "v3.8.0", diff --git a/config/bundles.php b/config/bundles.php index ed6283d..867e0e9 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -14,4 +14,8 @@ return [ Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], + Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true], + Symfony\UX\LiveComponent\LiveComponentBundle::class => ['all' => true], + Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], + Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], ]; diff --git a/config/packages/twig_component.yaml b/config/packages/twig_component.yaml new file mode 100644 index 0000000..fd17ac6 --- /dev/null +++ b/config/packages/twig_component.yaml @@ -0,0 +1,5 @@ +twig_component: + anonymous_template_directory: 'components/' + defaults: + # Namespace & directory for components + App\Twig\Components\: 'components/' diff --git a/config/routes/ux_live_component.yaml b/config/routes/ux_live_component.yaml new file mode 100644 index 0000000..e56523a --- /dev/null +++ b/config/routes/ux_live_component.yaml @@ -0,0 +1,5 @@ +live_component: + resource: '@LiveComponentBundle/config/routes.php' + prefix: '/_components' + # adjust prefix to add localization to your components + #prefix: '/{_locale}/_components' diff --git a/config/services.yaml b/config/services.yaml index 5bc44d9..827ce74 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -41,6 +41,10 @@ services: track_redirects: true + App\Service\MangaScraperServiceOld: + arguments: + $projectDir: '%kernel.project_dir%' + App\Service\MangaScraperService: arguments: $projectDir: '%kernel.project_dir%' @@ -55,3 +59,15 @@ services: App\Controller\MenuController: tags: [ 'controller.service_arguments' ] + + App\Client\MangadexClient: + arguments: + $httpClient: '@GuzzleHttp\Client' + $clientId: '%env(MANGADEX_CLIENT_ID)%' + $clientSecret: '%env(MANGADEX_CLIENT_SECRET)%' + $username: '%env(MANGADEX_USERNAME)%' + $password: '%env(MANGADEX_PASSWORD)%' + + App\Service\MangadexProvider: + arguments: + $client: '@App\Client\MangadexClient' diff --git a/frankenphp/conf.d/app.ini b/frankenphp/conf.d/app.ini index 501fa84..a99c9b8 100644 --- a/frankenphp/conf.d/app.ini +++ b/frankenphp/conf.d/app.ini @@ -12,3 +12,4 @@ opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 20000 opcache.memory_consumption = 256 opcache.enable_file_override = 1 +max_execution_time = 60 diff --git a/package-lock.json b/package-lock.json index 5457366..dea7531 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,9 +6,12 @@ "": { "license": "UNLICENSED", "dependencies": { + "@fortawesome/fontawesome-free": "^6.5.2", "alpinejs": "^3.13.3", "autoprefixer": "^10.4.14", + "bootstrap": "^5.3.3", "postcss-loader": "^7.1.0", + "puppeteer": "^22.10.0", "tailwindcss": "^3.2.7" }, "devDependencies": { @@ -16,6 +19,7 @@ "@babel/preset-env": "^7.16.0", "@hotwired/stimulus": "^3.0.0", "@symfony/stimulus-bridge": "^3.2.0", + "@symfony/ux-live-component": "file:vendor/symfony/ux-live-component/assets", "@symfony/webpack-encore": "^4.0.0", "core-js": "^3.23.0", "daisyui": "^4.4.2", @@ -1669,6 +1673,25 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.24.6.tgz", + "integrity": "sha512-tbC3o8uHK9xMgMsvUm9qGqxVpbv6yborMBLbDteHIc7JDNHsTV0vDMQ5j1O1NXvO+BDELtL9KgoWYaUVIVGt8w==", + "dev": true, + "dependencies": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3/node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, "node_modules/@babel/runtime/node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -1733,6 +1756,15 @@ "node": ">=10.0.0" } }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz", + "integrity": "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@hotwired/stimulus": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz", @@ -2027,6 +2059,88 @@ "node": ">=14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.3.tgz", + "integrity": "sha512-bJ0UBsk0ESOs6RFcLXOt99a3yTDcOKlzfjad+rhFwdaG1Lu/Wzq58GHYCDTlZ9z6mldf4g+NTb+TXEfe0PpnsQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.4.0", + "semver": "7.6.0", + "tar-fs": "3.0.5", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2052,6 +2166,10 @@ "@hotwired/stimulus": "^3.0" } }, + "node_modules/@symfony/ux-live-component": { + "resolved": "vendor/symfony/ux-live-component/assets", + "link": true + }, "node_modules/@symfony/webpack-encore": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/@symfony/webpack-encore/-/webpack-encore-4.6.1.tgz", @@ -2296,6 +2414,117 @@ "node": ">=8" } }, + "node_modules/@testing-library/dom": { + "version": "7.31.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz", + "integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^4.2.2", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.6", + "lz-string": "^1.4.4", + "pretty-format": "^26.6.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/user-event": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", + "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -2305,6 +2534,12 @@ "node": ">=10.13.0" } }, + "node_modules/@types/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -2464,6 +2699,16 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/node-forge": { "version": "1.3.11", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", @@ -2560,6 +2805,16 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vue/reactivity": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", @@ -2795,6 +3050,18 @@ "node": ">=8.9" } }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2923,6 +3190,19 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "engines": { + "node": ">=6.0" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -2967,6 +3247,24 @@ "webpack": ">=5.0.0" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", @@ -3003,6 +3301,12 @@ "postcss": "^8.1.0" } }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "license": "Apache-2.0" + }, "node_modules/babel-loader": { "version": "9.1.3", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", @@ -3117,6 +3421,81 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", + "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", + "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", + "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.0.1.tgz", + "integrity": "sha512-ubLyoDqPnUf5o0kSFp709HC0WRZuxVuh4pbte5eY95Xvx5bdvz07c2JFmXBfqqe60q+9PJ8S4X5GRvmcNSKMxg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.18.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -3207,6 +3586,24 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3259,6 +3656,39 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3403,6 +3833,20 @@ "node": ">=6.0" } }, + "node_modules/chromium-bidi": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.19.tgz", + "integrity": "sha512-UA6zL77b7RYCjJkZBsZ0wlvCTD+jTjllZ8f6wdO4buevXgTZYjV+XLB9CiEa2OuuTGGTLnI7eN9I60YxuALGQg==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.22.4" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -3433,6 +3877,70 @@ "webpack": ">=4.0.0 <6.0.0" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -3472,6 +3980,18 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -3628,6 +4148,17 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-js-pure": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.37.1.tgz", + "integrity": "sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -4027,11 +4558,19 @@ "url": "https://opencollective.com/daisyui" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -4082,6 +4621,20 @@ "node": ">=8" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/del": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", @@ -4100,6 +4653,15 @@ "node": ">=6" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4125,6 +4687,12 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/devtools-protocol": { + "version": "0.0.1286932", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1286932.tgz", + "integrity": "sha512-wu58HMQll9voDjR4NlPyoDEw1syfzaBNHymMMZ/QOXiHRNluOnDgu9hp1yHOKYoMlxCh4lSSiugLITe6Fvu1eA==", + "license": "BSD-3-Clause" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4147,6 +4715,12 @@ "node": ">=6" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -4250,6 +4824,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.16.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", @@ -4271,6 +4854,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/envinfo": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", @@ -4352,6 +4944,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -4364,6 +4986,19 @@ "node": ">=8.0.0" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -4395,7 +5030,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4503,11 +5137,52 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -4572,6 +5247,15 @@ "node": ">=0.8.0" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4703,6 +5387,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4733,6 +5431,20 @@ "node": ">= 0.6" } }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs-monkey": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", @@ -4775,6 +5487,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -4815,6 +5536,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5089,6 +5825,19 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http-proxy-middleware": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", @@ -5113,6 +5862,19 @@ } } }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -5146,6 +5908,32 @@ "postcss": "^8.1.0" } }, + "node_modules/idiomorph": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.3.0.tgz", + "integrity": "sha512-UhV1Ey5xCxIwR9B+OgIjQa+1Jx99XQ1vQHUsKBU1RpQzCx1u+b+N6SOXgf5mEJDqemUI/ffccu6+71l2mJUsRA==", + "dev": true + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/immutable": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", @@ -5285,6 +6073,19 @@ "node": ">= 0.10" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -5639,6 +6440,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -5673,6 +6480,18 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -5778,6 +6597,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -5984,11 +6812,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -6044,6 +6877,35 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -6180,7 +7042,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -6278,6 +7139,38 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6389,6 +7282,12 @@ "node": ">=8" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -7169,12 +8068,131 @@ "renderkid": "^3.0.0" } }, + "node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/pretty-format/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/pretty-format/node_modules/@types/yargs": { + "version": "15.0.19", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", + "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/pretty-format/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/pretty-format/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7197,6 +8215,50 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7205,6 +8267,84 @@ "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.10.0.tgz", + "integrity": "sha512-ZOkZd6a6t0BdKcWb0wAYHWQqCfdlN1PPnXOmg/XNrbo6gJhYWFX4qCNb6ahSn8TpAqBqLCoD4Q010F7GwOM7mA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.2.3", + "cosmiconfig": "9.0.0", + "devtools-protocol": "0.0.1286932", + "puppeteer-core": "22.10.0" + }, + "bin": { + "puppeteer": "lib/esm/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.10.0.tgz", + "integrity": "sha512-I54J4Vy4I07UHsgB1QSmuFoF7KNQjJWcvFBPhtY+ezMdBfwgGDr8dzYrJa11aPgP9kxIUHjhktcMmmfJkOAtTw==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.2.3", + "chromium-bidi": "0.5.19", + "debug": "4.3.4", + "devtools-protocol": "0.0.1286932", + "ws": "8.17.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/puppeteer/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -7239,6 +8379,12 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7280,6 +8426,12 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -7423,6 +8575,15 @@ "strip-ansi": "^6.0.1" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -7921,6 +9082,16 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -7932,6 +9103,34 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7987,6 +9186,12 @@ "wbuf": "^1.7.3" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -8002,6 +9207,20 @@ "node": ">= 0.8" } }, + "node_modules/streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -8364,6 +9583,31 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/terser": { "version": "5.31.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz", @@ -8454,6 +9698,15 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -8473,6 +9726,12 @@ "node": ">=0.8" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -8517,11 +9776,23 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -8535,6 +9806,16 @@ "node": ">= 0.6" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -8580,6 +9861,15 @@ "node": ">=4" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -8626,6 +9916,12 @@ "punycode": "^2.1.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8685,6 +9981,12 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, "node_modules/webpack": { "version": "5.91.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", @@ -9045,6 +10347,16 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9188,14 +10500,12 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", - "dev": true, "engines": { "node": ">=10.0.0" }, @@ -9212,6 +10522,15 @@ } } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -9229,15 +10548,42 @@ "node": ">= 14" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", @@ -9249,6 +10595,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "vendor/symfony/ux-live-component/assets": { + "name": "@symfony/ux-live-component", + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "idiomorph": "^0.3.0" + }, + "devDependencies": { + "@hotwired/stimulus": "^3.0.0", + "@testing-library/dom": "^7.31.0", + "@testing-library/user-event": "^13.1.9", + "@types/node-fetch": "^2.6.2", + "node-fetch": "^2.6.1" + }, + "peerDependencies": { + "@hotwired/stimulus": "^3.0.0" + } } } } diff --git a/package.json b/package.json index 807d8ea..7250549 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "@babel/preset-env": "^7.16.0", "@hotwired/stimulus": "^3.0.0", "@symfony/stimulus-bridge": "^3.2.0", + "@symfony/ux-live-component": "file:vendor/symfony/ux-live-component/assets", "@symfony/webpack-encore": "^4.0.0", "core-js": "^3.23.0", "daisyui": "^4.4.2", @@ -23,9 +24,12 @@ "build": "encore production --progress" }, "dependencies": { + "@fortawesome/fontawesome-free": "^6.5.2", "alpinejs": "^3.13.3", "autoprefixer": "^10.4.14", + "bootstrap": "^5.3.3", "postcss-loader": "^7.1.0", + "puppeteer": "^22.10.0", "tailwindcss": "^3.2.7" } } diff --git a/public/img/mangarr_logo.png b/public/img/mangarr_logo.png new file mode 100644 index 0000000..42a786e Binary files /dev/null and b/public/img/mangarr_logo.png differ diff --git a/public/puppeteer-script.js b/public/puppeteer-script.js new file mode 100644 index 0000000..acd2874 --- /dev/null +++ b/public/puppeteer-script.js @@ -0,0 +1,89 @@ +const puppeteer = require('puppeteer'); + +(async () => { + try { + // Récupérer les paramètres de la ligne de commande + const [url, imageSelector, nextButtonSelector] = process.argv.slice(2); + + console.log('URL:', url); + console.log('Image Selector:', imageSelector); + console.log('Next Button Selector:', nextButtonSelector); + + const browser = await puppeteer.launch({ + headless: true, + executablePath: process.env.CHROME_BIN || null, + args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'] + }); + + console.log('Browser launched'); + + const page = await browser.newPage(); + console.log('New page created'); + + await page.goto(url, {waitUntil: 'networkidle2'}); + console.log('Page loaded'); + + // Function to scroll to the bottom of the page + async function autoScroll(page){ + await page.evaluate(async () => { + await new Promise((resolve, reject) => { + var totalHeight = 0; + var distance = 100; + var timer = setInterval(() => { + var scrollHeight = document.body.scrollHeight; + window.scrollBy(0, distance); + totalHeight += distance; + + if(totalHeight >= scrollHeight){ + clearInterval(timer); + resolve(); + } + }, 100); + }); + }); + } + + // Attendre que le conteneur des images soit présent pour s'assurer que la page est complètement chargée + await page.waitForSelector(imageSelector); + + console.log('Image selector found'); + + const imageUrls = new Set(); // Utiliser un Set pour éviter les doublons + + // let previousImageUrl = null; + while (true) { + console.log('Fetching images'); + const pageImageUrls = await page.$$eval(imageSelector, imgs => + imgs.map(img => img.getAttribute('src') || img.getAttribute('data-src')) + ); + + console.log('Page image URLs:', pageImageUrls); + + pageImageUrls.forEach(url => { + if (!imageUrls.has(url)) { + imageUrls.add(url); + console.log('Image URL:', url); + } + }); + + // Cliquer sur le bouton suivant + const nextButton = await page.$(nextButtonSelector); + console.log('Next button'); + if (!nextButton) { + console.log(page); + break; // Sortir de la boucle si le bouton suivant n'existe pas + } + console.log('Clicking next button'); + await nextButton.click(); + // await page.waitForTimeout(1000); // Attendre 1 seconde + await page.waitForSelector(imageSelector); + } + + console.log('Image URLs:', JSON.stringify(Array.from(imageUrls))); + + await browser.close(); + } catch (error) { + console.error('Error:', error); + process.exit(1); // Exit with a failure code + } +})(); diff --git a/src/Client/MangadexClient.php b/src/Client/MangadexClient.php new file mode 100644 index 0000000..26a31da --- /dev/null +++ b/src/Client/MangadexClient.php @@ -0,0 +1,86 @@ +httpClient = $httpClient; + $this->clientId = $clientId; + $this->clientSecret = $clientSecret; + $this->username = $username; + $this->password = $password; + $this->authenticate(); + } + + public function authenticate(): void + { + $response = $this->httpClient->request('POST', self::AUTHENTICATION_URL, [ + 'form_params' => [ + 'grant_type' => 'password', + 'username' => $this->username, + 'password' => $this->password, + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ], + ]); + + $data = json_decode($response->getBody()->getContents(), true); + $this->accessToken = $data['access_token']; + $this->refreshToken = $data['refresh_token']; + } + + public function refresh(): void + { + $response = $this->httpClient->request('POST', self::AUTHENTICATION_URL, [ + 'form_params' => [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $this->refreshToken, + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ], + ]); + + $data = json_decode($response->getBody()->getContents(), true); + $this->accessToken = $data['access_token']; + } + + private function request(string $method, string $endpoint, array $options = []): array + { + $options['headers']['Authorization'] = 'Bearer ' . $this->accessToken; + + $response = $this->httpClient->request($method, self::API_URL . $endpoint, $options); + + if ($response->getStatusCode() === 429) { + $this->refresh(); + $options['headers']['Authorization'] = 'Bearer ' . $this->accessToken; + $response = $this->httpClient->request($method, self::API_URL . $endpoint, $options); + } + + return json_decode($response->getBody()->getContents(), true); + } + + public function get(string $endpoint, array $params = []): array + { + return $this->request('GET', $endpoint, ['query' => $params]); + } + + public function post(string $endpoint, array $data): array + { + return $this->request('POST', $endpoint, ['json' => $data]); + } +} diff --git a/src/Controller/MangaController.php b/src/Controller/MangaController.php index 5205d39..9d1accd 100644 --- a/src/Controller/MangaController.php +++ b/src/Controller/MangaController.php @@ -6,8 +6,8 @@ use App\Entity\Manga; use App\Repository\MangaRepository; use App\Service\MangaExportService; use App\Service\LelScansProviderService; -use App\Service\MangaScraperService; -use App\Service\MangaUpdatesDbProvider; +use App\Service\MangaScraperServiceOld; +use App\Service\MangaUpdatesMetadataProvider; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Request; @@ -20,11 +20,11 @@ use Symfony\Component\String\Slugger\AsciiSlugger; class MangaController extends AbstractController { public function __construct( - private readonly MangaScraperService $mangaScraperService, + private readonly MangaScraperServiceOld $mangaScraperService, private readonly MangaExportService $mangaExportService, private readonly LelScansProviderService $mangaProviderService, private readonly MangaRepository $mangaRepository, - private MangaUpdatesDbProvider $mangaUpdatesDbProvider + private MangaUpdatesMetadataProvider $mangaUpdatesDbProvider ) { } @@ -48,12 +48,17 @@ class MangaController extends AbstractController throw new NotFoundHttpException("Le manga demandé n'existe pas."); } - $availableChapters = $this->mangaProviderService->getChapterList($mangaSlug); + $chaptersByVolume = []; + foreach ($manga->getChapters() as $chapter) { + $volume = $chapter->getVolume() ?? 'Not Found'; + $chaptersByVolume[$volume][] = $chapter; + } + + $chaptersByVolume = array_map('array_reverse', array_reverse($chaptersByVolume, true)); return $this->render('manga/show_chapters.html.twig', [ - 'controller_name' => 'MangaController', + 'chapters_by_volume' => $chaptersByVolume, 'manga' => $manga, - 'availableChapters' => $availableChapters, ]); } @@ -83,19 +88,11 @@ class MangaController extends AbstractController ]); } - #[Route('/addNew', name: 'add_new_manga')] - public function addNew(): Response + #[Route('/addNew/{query}', name: 'add_new_manga')] + public function addNew(string $query = ''): Response { - $availableManga = $this->mangaProviderService->getMangaList(); - - foreach ($availableManga as $key => $manga) { - $availableManga[$key]['slug'] = $this->titleToSlug($manga['name']); - } - - $mangas = $this->mangaRepository->findAll(); return $this->render('manga/add_new.html.twig', [ - 'availableManga' => $availableManga, - 'mangas' => $mangas, + 'query' => $query, ]); } diff --git a/src/Controller/TestController.php b/src/Controller/TestController.php new file mode 100644 index 0000000..8afe679 --- /dev/null +++ b/src/Controller/TestController.php @@ -0,0 +1,30 @@ +mangaRepository->find(8); + + dd($this->mangadexProvider->getFeed($manga)); + } +} diff --git a/src/Entity/Chapter.php b/src/Entity/Chapter.php index baa7cb2..4a159cf 100644 --- a/src/Entity/Chapter.php +++ b/src/Entity/Chapter.php @@ -28,6 +28,15 @@ class Chapter #[ORM\OneToMany(mappedBy: 'chapter', targetEntity: Page::class, orphanRemoval: true)] private Collection $pagesLink; + #[ORM\Column(nullable: true)] + private ?int $volume = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $title = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $localPath = null; + public function __construct() { $this->pagesLink = new ArrayCollection(); @@ -105,16 +114,52 @@ class Chapter } public function getPageByNumber(int $number): ?Page - { - /** - * @var Page $page - */ - foreach ($this->pagesLink as $page) { - if ($page->getNumber() === $number) { - return $page; - } - } + { + /** + * @var Page $page + */ + foreach ($this->pagesLink as $page) { + if ($page->getNumber() === $number) { + return $page; + } + } + + return null; + } - return null; - } + public function getVolume(): ?int + { + return $this->volume; + } + + public function setVolume(?int $volume): static + { + $this->volume = $volume; + + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): static + { + $this->title = $title; + + return $this; + } + + public function getLocalPath(): ?string + { + return $this->localPath; + } + + public function setLocalPath(?string $localPath): static + { + $this->localPath = $localPath; + + return $this; + } } diff --git a/src/Entity/ContentSource.php b/src/Entity/ContentSource.php new file mode 100644 index 0000000..5f90073 --- /dev/null +++ b/src/Entity/ContentSource.php @@ -0,0 +1,100 @@ +id; + } + + public function getBaseUrl(): ?string + { + return $this->baseUrl; + } + + public function setBaseUrl(string $baseUrl): static + { + $this->baseUrl = $baseUrl; + + return $this; + } + + public function getImageSelector(): ?string + { + return $this->imageSelector; + } + + public function setImageSelector(string $imageSelector): static + { + $this->imageSelector = $imageSelector; + + return $this; + } + + public function getNextPageSelector(): ?string + { + return $this->NextPageSelector; + } + + public function setNextPageSelector(string $NextPageSelector): static + { + $this->NextPageSelector = $NextPageSelector; + + return $this; + } + + public function getChapterUrlFormat(): ?string + { + return $this->chapterUrlFormat; + } + + public function setChapterUrlFormat(string $chapterUrlFormat): static + { + $this->chapterUrlFormat = $chapterUrlFormat; + + return $this; + } + + public function getChapterUrl(string $mangaTitle, float $chapterNumber): string + { + return sprintf($this->chapterUrlFormat, $mangaTitle, $chapterNumber); + } + + public function getScrapingType(): ?string + { + return $this->scrapingType; + } + + public function setScrapingType(string $scrapingType): static + { + $this->scrapingType = $scrapingType; + + return $this; + } +} diff --git a/src/Entity/Manga.php b/src/Entity/Manga.php index 48b11e3..05c516c 100644 --- a/src/Entity/Manga.php +++ b/src/Entity/Manga.php @@ -22,7 +22,7 @@ class Manga #[ORM\OneToMany(mappedBy: 'manga', targetEntity: Chapter::class, orphanRemoval: true)] private Collection $chapters; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, unique: true)] private ?string $slug = null; #[ORM\Column(length: 255, nullable: true)] @@ -37,9 +37,25 @@ class Manga #[ORM\Column(type: Types::ARRAY, nullable: true)] private ?array $genres = null; + #[ORM\Column] + private ?\DateTimeImmutable $createdAt = null; + + #[ORM\Column(nullable: true)] + private ?float $rating = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $author = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $externalId = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $status = null; + public function __construct() { $this->chapters = new ArrayCollection(); + $this->createdAt = new \DateTimeImmutable(); } public function getId(): ?int @@ -67,15 +83,15 @@ class Manga return $this->chapters; } - public function getChapterByNumber(float $number): ?Chapter - { - foreach ($this->chapters as $chapter) { - if ($chapter->getNumber() === $number) { - return $chapter; - } - } - return null; - } + public function getChapterByNumber(float $number): ?Chapter + { + foreach ($this->chapters as $chapter) { + if ($chapter->getNumber() === $number) { + return $chapter; + } + } + return null; + } public function addChapter(Chapter $chapter): self { @@ -158,4 +174,64 @@ class Manga return $this; } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getRating(): ?float + { + return $this->rating; + } + + public function setRating(?float $rating): static + { + $this->rating = $rating; + + return $this; + } + + public function getAuthor(): ?string + { + return $this->author; + } + + public function setAuthor(?string $author): static + { + $this->author = $author; + + return $this; + } + + public function getExternalId(): ?string + { + return $this->externalId; + } + + public function setExternalId(?string $externalId): static + { + $this->externalId = $externalId; + + return $this; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(?string $status): static + { + $this->status = $status; + + return $this; + } } diff --git a/src/EventListener/ExceptionListener.php b/src/EventListener/ExceptionListener.php index 3830335..5bffc1d 100644 --- a/src/EventListener/ExceptionListener.php +++ b/src/EventListener/ExceptionListener.php @@ -21,24 +21,24 @@ class ExceptionListener public function onKernelException(ExceptionEvent $event): void { - $exception = $event->getThrowable(); - - $response = match(true) { - $exception instanceof FilterValidationException, - $exception instanceof BadRequestException => $this->createResponse($exception, Response::HTTP_BAD_REQUEST), - $exception instanceof NotFoundHttpException, - $exception instanceof ItemNotFoundException => $this->createResponse($exception, Response::HTTP_NOT_FOUND), - $exception instanceof AccessDeniedHttpException => $this->createResponse($exception, Response::HTTP_FORBIDDEN), - $exception instanceof ValidationException, - $exception instanceof NotNormalizableValueException => $this->createResponse($exception, Response::HTTP_UNPROCESSABLE_ENTITY), - default => null, - }; - - if ($response) { - $event->setResponse($response); - }else{ - $this->logger->error($exception->getMessage(), ['exception' => $exception]); - } +// $exception = $event->getThrowable(); +// +// $response = match(true) { +// $exception instanceof FilterValidationException, +// $exception instanceof BadRequestException => $this->createResponse($exception, Response::HTTP_BAD_REQUEST), +// $exception instanceof NotFoundHttpException, +// $exception instanceof ItemNotFoundException => $this->createResponse($exception, Response::HTTP_NOT_FOUND), +// $exception instanceof AccessDeniedHttpException => $this->createResponse($exception, Response::HTTP_FORBIDDEN), +// $exception instanceof ValidationException, +// $exception instanceof NotNormalizableValueException => $this->createResponse($exception, Response::HTTP_UNPROCESSABLE_ENTITY), +// default => null, +// }; +// +// if ($response) { +// $event->setResponse($response); +// }else{ +// $this->logger->error($exception->getMessage(), ['exception' => $exception]); +// } } private function createResponse(\Throwable $exception, int $statusCode): Response diff --git a/src/Factory/MangaFactory.php b/src/Factory/MangaFactory.php index 8e5dd87..3b0b175 100644 --- a/src/Factory/MangaFactory.php +++ b/src/Factory/MangaFactory.php @@ -52,6 +52,10 @@ final class MangaFactory extends ModelFactory return [ 'slug' => $this->slugger->slug($title)->lower(), 'title' => $title, + 'description' => self::faker()->text(), + 'genres' => self::faker()->words(rand(1, 5)), + 'publicationYear' => self::faker()->year(), + 'rating' => self::faker()->randomFloat(1, 0, 10), ]; } diff --git a/src/Factory/PageFactory.php b/src/Factory/PageFactory.php index 5192350..4e56d05 100644 --- a/src/Factory/PageFactory.php +++ b/src/Factory/PageFactory.php @@ -48,8 +48,8 @@ final class PageFactory extends ModelFactory { return [ 'chapter' => ChapterFactory::new(), - 'imageLocalUrl' => self::faker()->text(255), - 'imageUrl' => self::faker()->text(255), + 'imageLocalUrl' => 'https://placehold.co/770x1090', + 'imageUrl' => 'https://placehold.co/770x1090', 'number' => self::faker()->randomNumber(2), ]; } diff --git a/src/Interface/ClientInterface.php b/src/Interface/ClientInterface.php new file mode 100644 index 0000000..f38aa92 --- /dev/null +++ b/src/Interface/ClientInterface.php @@ -0,0 +1,9 @@ + + * + * @method ContentSource|null find($id, $lockMode = null, $lockVersion = null) + * @method ContentSource|null findOneBy(array $criteria, array $orderBy = null) + * @method ContentSource[] findAll() + * @method ContentSource[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class ContentSourceRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ContentSource::class); + } + +// /** +// * @return ContentSource[] Returns an array of ContentSource objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('c') +// ->andWhere('c.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('c.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?ContentSource +// { +// return $this->createQueryBuilder('c') +// ->andWhere('c.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/src/Repository/MangaRepository.php b/src/Repository/MangaRepository.php index 06b60da..60bb6e1 100644 --- a/src/Repository/MangaRepository.php +++ b/src/Repository/MangaRepository.php @@ -39,6 +39,15 @@ class MangaRepository extends ServiceEntityRepository } } + public function findByTitle(string $title): array + { + return $this->createQueryBuilder('m') + ->andWhere('m.title LIKE :title') + ->setParameter('title', "%$title%") + ->getQuery() + ->getResult(); + } + // /** // * @return Manga[] Returns an array of Manga objects // */ diff --git a/src/Service/LelScansProviderService.php b/src/Service/LelScansProviderService.php index b9ca605..cdeca56 100644 --- a/src/Service/LelScansProviderService.php +++ b/src/Service/LelScansProviderService.php @@ -1,10 +1,12 @@ new LelScansProviderService(), @@ -12,4 +14,4 @@ class MangaProviderFactory default => throw new \Exception("Provider {$providerName} non supporté."), }; } -} \ No newline at end of file +} diff --git a/src/Service/MangaProviderInterface.php b/src/Service/MangaProviderInterface.php deleted file mode 100644 index 8336049..0000000 --- a/src/Service/MangaProviderInterface.php +++ /dev/null @@ -1,9 +0,0 @@ -projectDir = $projectDir; - $this->eventDispatcher = $eventDispatcher; - } + public function __construct($projectDir, EventDispatcherInterface $eventDispatcher) + { + $this->projectDir = $projectDir; + $this->eventDispatcher = $eventDispatcher; + } - public function extractMangaPageData(string $html): array - { - $baseUrl = 'https://lelscans.net'; - //pour éviter à PhpStorm de gueuler... - $selector = 'img'; - $crawler = new Crawler($html); - $imgUrl = $crawler->filter($selector)->attr('src'); - $nextLink = $crawler->filter('a[title="Suivant"]'); + public function extractMangaPageData(string $html, ContentSource $mangaSource): array + { + $crawler = new Crawler($html); + $imgUrls = []; - if (!preg_match('/^https?:\/\//', $imgUrl)) { - $urlComponents = parse_url($baseUrl); - $scheme = $urlComponents['scheme']; - $host = $urlComponents['host']; + // Search for images with different extensions + foreach (['img[src$=".jpg"]', 'img[src$=".jpeg"]', 'img[src$=".png"]', 'img'] as $selector) { + $crawler->filter($selector)->each(function (Crawler $node) use (&$imgUrls) { + $src = $node->attr('src') ?? $node->attr('data-src'); + if ($src) { + $imgUrls[] = $src; + } + }); + } - // Construit l'URL absolue de l'image - $imgUrl = $scheme . '://' . $host . '/' . ltrim($imgUrl, '/'); - } + if (empty($imgUrls)) { + throw new \Exception('No valid image found on the page.'); + } - if($nextLink->count() > 0){ - $nextUrl = $nextLink->attr('href'); - }else{ - $nextUrl = null; - } + $nextLink = $crawler->filter($mangaSource->getNextPageSelector()); + $nextUrl = $nextLink->count() > 0 ? $nextLink->attr('href') : null; - return [ - 'image_url' => $imgUrl, - 'next_page_url' => $nextUrl, - ]; - } + // Convert relative URLs to absolute URLs + $baseUrl = $mangaSource->getBaseUrl(); + $imgUrls = array_map(function ($imgUrl) use ($baseUrl) { + if (!preg_match('/^https?:\/\//', $imgUrl)) { + $urlComponents = parse_url($baseUrl); + $scheme = $urlComponents['scheme']; + $host = $urlComponents['host']; + $imgUrl = $scheme . '://' . $host . '/' . ltrim($imgUrl, '/'); + } + return $imgUrl; + }, $imgUrls); + + return [ + 'image_urls' => $imgUrls, + 'next_page_url' => $nextUrl, + ]; + } /** * @throws GuzzleException */ - public function scrapeMangaChapter(string $chapterUrl, string $mangaTitle, float $chapterNumber): array|bool - { - if(!$this->isChapterAvailable($chapterUrl, $chapterNumber)){ - return false; - } + public function scrapeManga(Manga $manga, ContentSource $mangaSource): array + { + $allChaptersData = []; - $pageData = []; - $currentPageUrl = $chapterUrl; + foreach ($manga->getChapters() as $chapter) { + $chapterData = $this->scrapeChapter($manga, $chapter, $mangaSource); + if ($chapterData !== false) { + $allChaptersData[$chapter->getNumber()] = $chapterData; + } + } - $mangaDir = sprintf('%s/%s', $this->projectDir . self::IMG_BASE_DIR, $mangaTitle); - if (!is_dir($mangaDir)) { - mkdir($mangaDir, 0755, true); - } + return $allChaptersData; + } - // Créez le dossier du chapitre s'il n'existe pas - $chapterDir = sprintf('%s/%s', $mangaDir, $chapterNumber); - if (!is_dir($chapterDir)) { - mkdir($chapterDir, 0755, true); - } + private function scrapeChapter(Manga $manga, Chapter $chapter, ContentSource $mangaSource): array|bool + { + switch ($mangaSource->getScrapingType()) { + case 'html': + return $this->scrapeChapterHtml($manga, $chapter, $mangaSource); + case 'javascript': + return $this->scrapeChapterJavaScript($manga, $chapter, $mangaSource); +// case 'api': +// // Implémentez la méthode de scraping par API si nécessaire +// return $this->scrapeChapterApi($manga, $chapter, $mangaSource); + default: + throw new \Exception('Unsupported scraping type: ' . $mangaSource->getScrapingType()); + } + } - do { - $html = $this->fetchHtml($currentPageUrl); - $page = $this->extractMangaPageData($html); - $pageData[] = $page; - $currentPageUrl = $page['next_page_url']; +// private function scrapeChapterHtml(Manga $manga, Chapter $chapter, MangaSource $mangaSource): array|bool +// { +// $chapterUrl = $mangaSource->getChapterUrl($manga->getTitle(), $chapter->getChapterNumber()); +// $html = $this->fetchHtml($chapterUrl); +// $imgUrls = $this->extractMangaPageData($html); +// +// return $this->saveChapterImages($manga, $chapter, $imgUrls); +// } - // Construisez le nom de fichier de l'image - $imageName = sprintf('%03d.jpg', count($pageData)); + private function scrapeChapterJavaScript(Manga $manga, Chapter $chapter, ContentSource $mangaSource): array|bool + { + $chapterUrl = $mangaSource->getChapterUrl($manga->getTitle(), $chapter->getNumber()); + $imgUrls = $this->fetchImagesUsingPuppeteer($chapterUrl, $mangaSource->getImageSelector(), $mangaSource->getNextPageSelector()); - // Construisez le chemin du fichier de l'image - $imagePath = sprintf('%s/%s', $chapterDir, $imageName); + return $this->saveChapterImages($manga, $chapter, $imgUrls); + } - // Téléchargez et enregistrez l'image - $this->downloadAndSaveImage($page['image_url'], $imagePath); + private function fetchImagesUsingPuppeteer(string $url, string $imageSelector, string $nextButtonSelector): array + { + // Appeler le script Puppeteer avec les paramètres nécessaires + $output = []; + $command = sprintf('node puppeteer-script.js "%s" "%s" "%s" 2>&1', $url, $imageSelector, $nextButtonSelector); // Redirect stderr to stdout + dump($command); +// exec($command, $output, $return_var); - // Modifiez les données de la page pour inclure l'URL de l'image stockée localement - $pageData[count($pageData) - 1]['local_image_url'] = sprintf('/manga-images/%s/%s/%s', $mangaTitle, $chapterNumber, $imageName); - $pageData[count($pageData) - 1]['page_number'] = count($pageData); + dd($command, $output); - } while ($currentPageUrl); + // Convertir la sortie JSON en tableau PHP + return json_decode(implode("", $output), true); + } - $event = new MangaScrapedEvent($mangaTitle, $chapterNumber, $pageData); - $this->eventDispatcher->dispatch($event, MangaScrapedEvent::NAME); + /** + * @throws GuzzleException + */ + private function scrapeChapterHtml(Manga $manga, Chapter $chapter, ContentSource $mangaSource): array|bool + { + $chapterUrl = $mangaSource->getChapterUrl($manga->getSlug(), $chapter->getNumber()); - return $pageData; - } + $pageData = []; + $currentPageUrl = $chapterUrl; + $mangaTitle = $manga->getTitle(); + $chapterNumber = $chapter->getNumber(); + + $mangaDir = sprintf('%s/%s', $this->projectDir . self::IMG_BASE_DIR, $mangaTitle); + if (!is_dir($mangaDir)) { + mkdir($mangaDir, 0755, true); + } + + $chapterDir = sprintf('%s/%s', $mangaDir, $chapterNumber); + if (!is_dir($chapterDir)) { + mkdir($chapterDir, 0755, true); + } + + do { + $html = $this->fetchHtml($currentPageUrl); + $page = $this->extractMangaPageData($html, $mangaSource); + + foreach ($page['image_urls'] as $imgUrl) { + dump($imgUrl); + dump(base64_decode($imgUrl)); + // Déterminer l'extension de l'image + $imageExtension = pathinfo(parse_url($imgUrl, PHP_URL_PATH), PATHINFO_EXTENSION); + + // Construire le nom de fichier de l'image + $imageName = sprintf('%03d.%s', count($pageData) + 1, $imageExtension); + $imagePath = sprintf('%s/%s', $chapterDir, $imageName); + + $this->downloadAndSaveImage($imgUrl, $imagePath); + + $pageData[] = [ + 'image_url' => $imgUrl, + 'local_image_url' => sprintf('/manga-images/%s/%s/%s', $mangaTitle, $chapterNumber, $imageName), + 'page_number' => count($pageData) + 1, + ]; + } + + // Si plus d'une image a été trouvée, ne pas chercher la page suivante + if (count($page['image_urls']) > 1) { + break; + } + + $currentPageUrl = $page['next_page_url']; + } while ($currentPageUrl); + + $event = new MangaScrapedEvent($mangaTitle, $chapterNumber, $pageData); + $this->eventDispatcher->dispatch($event, MangaScrapedEvent::NAME); + + return $pageData; + } /** * @throws GuzzleException */ private function fetchHtml(string $url): string - { - $client = new Client(); - $response = $client->get($url); + { + $client = new Client(); + $response = $client->get($url); - return (string) $response->getBody(); - } + return (string)$response->getBody(); + } /** * @throws GuzzleException */ private function downloadAndSaveImage(string $imageUrl, string $destinationPath): void - { - $client = new Client(); - $response = $client->get($imageUrl); + { + $client = new Client(); + $response = $client->get($imageUrl); - file_put_contents($destinationPath, $response->getBody()->getContents()); - } + file_put_contents($destinationPath, $response->getBody()->getContents()); + } + + private function saveChapterImages(Manga $manga, Chapter $chapter, array $imgUrls): array + { + $mangaTitle = $manga->getTitle(); + $chapterNumber = $chapter->getNumber(); + + $mangaDir = sprintf('%s/%s', $this->projectDir . self::IMG_BASE_DIR, $mangaTitle); + if (!is_dir($mangaDir)) { + mkdir($mangaDir, 0755, true); + } + + $chapterDir = sprintf('%s/%s', $mangaDir, $chapterNumber); + if (!is_dir($chapterDir)) { + mkdir($chapterDir, 0755, true); + } + + $pageData = []; + foreach ($imgUrls as $index => $imgUrl) { + $imageName = sprintf('%03d.%s', $index + 1, pathinfo(parse_url($imgUrl, PHP_URL_PATH), PATHINFO_EXTENSION)); + $imagePath = sprintf('%s/%s', $chapterDir, $imageName); + + $this->downloadAndSaveImage($imgUrl, $imagePath); + + $pageData[] = [ + 'image_url' => $imgUrl, + 'local_image_url' => sprintf('/manga-images/%s/%s/%s', $mangaTitle, $chapterNumber, $imageName), + 'page_number' => $index + 1, + ]; + } + + $event = new MangaScrapedEvent($mangaTitle, $chapterNumber, $pageData); + $this->eventDispatcher->dispatch($event, MangaScrapedEvent::NAME); + + return $pageData; + } /** * @throws GuzzleException */ - private function isChapterAvailable(string $chapterUrl, float $chapterNumber): bool - { - $html = $this->fetchHtml($chapterUrl); - $crawler = new Crawler($html); - $nextLink = $crawler->filter('a[title="Suivant"]'); + private function isChapterAvailable(string $chapterUrl, float $chapterNumber, ContentSource $mangaSource): bool + { + $html = $this->fetchHtml($chapterUrl); + $crawler = new Crawler($html); + $nextLink = $crawler->filter($mangaSource->getNextPageSelector()); - if($nextLink->count() === 0){ - return false; - }else{ - $nextUrl = $nextLink->attr('href'); - } + if ($nextLink->count() === 0) { + return false; + } - $routeCollection = new RouteCollection(); - $routeCollection->add('manga_chapter', new Route('/scan-{manga}/{chapter}/{page}')); - $context = new RequestContext('/'); - $matcher = new UrlMatcher($routeCollection, $context); - $path = parse_url($nextUrl, PHP_URL_PATH); - $parameters = $matcher->match($path); + $nextUrl = $nextLink->attr('href'); + $routeCollection = new RouteCollection(); + $routeCollection->add('manga_chapter', new Route('/scan-{manga}/{chapter}/{page}')); + $context = new RequestContext('/'); + $matcher = new UrlMatcher($routeCollection, $context); + $path = parse_url($nextUrl, PHP_URL_PATH); + $parameters = $matcher->match($path); - if((float) $parameters['chapter'] !== $chapterNumber){ - return false; - } - - return true; - } + return (float)$parameters['chapter'] === $chapterNumber; + } } diff --git a/src/Service/MangaScraperServiceOld.php b/src/Service/MangaScraperServiceOld.php new file mode 100644 index 0000000..bd06161 --- /dev/null +++ b/src/Service/MangaScraperServiceOld.php @@ -0,0 +1,157 @@ +projectDir = $projectDir; + $this->eventDispatcher = $eventDispatcher; + } + + public function extractMangaPageData(string $html): array + { + $baseUrl = 'https://lelscans.net'; + //pour éviter à PhpStorm de gueuler... + $selector = 'img'; + $crawler = new Crawler($html); + $imgUrl = $crawler->filter($selector)->attr('src'); + $nextLink = $crawler->filter('a[title="Suivant"]'); + + if (!preg_match('/^https?:\/\//', $imgUrl)) { + $urlComponents = parse_url($baseUrl); + $scheme = $urlComponents['scheme']; + $host = $urlComponents['host']; + + // Construit l'URL absolue de l'image + $imgUrl = $scheme . '://' . $host . '/' . ltrim($imgUrl, '/'); + } + + if($nextLink->count() > 0){ + $nextUrl = $nextLink->attr('href'); + }else{ + $nextUrl = null; + } + + return [ + 'image_url' => $imgUrl, + 'next_page_url' => $nextUrl, + ]; + } + + /** + * @throws GuzzleException + */ + public function scrapeMangaChapter(string $chapterUrl, string $mangaTitle, float $chapterNumber): array|bool + { + if(!$this->isChapterAvailable($chapterUrl, $chapterNumber)){ + return false; + } + + $pageData = []; + $currentPageUrl = $chapterUrl; + + $mangaDir = sprintf('%s/%s', $this->projectDir . self::IMG_BASE_DIR, $mangaTitle); + if (!is_dir($mangaDir)) { + mkdir($mangaDir, 0755, true); + } + + // Créez le dossier du chapitre s'il n'existe pas + $chapterDir = sprintf('%s/%s', $mangaDir, $chapterNumber); + if (!is_dir($chapterDir)) { + mkdir($chapterDir, 0755, true); + } + + do { + $html = $this->fetchHtml($currentPageUrl); + $page = $this->extractMangaPageData($html); + $pageData[] = $page; + $currentPageUrl = $page['next_page_url']; + + // Construisez le nom de fichier de l'image + $imageName = sprintf('%03d.jpg', count($pageData)); + + // Construisez le chemin du fichier de l'image + $imagePath = sprintf('%s/%s', $chapterDir, $imageName); + + // Téléchargez et enregistrez l'image + $this->downloadAndSaveImage($page['image_url'], $imagePath); + + // Modifiez les données de la page pour inclure l'URL de l'image stockée localement + $pageData[count($pageData) - 1]['local_image_url'] = sprintf('/manga-images/%s/%s/%s', $mangaTitle, $chapterNumber, $imageName); + $pageData[count($pageData) - 1]['page_number'] = count($pageData); + + } while ($currentPageUrl); + + $event = new MangaScrapedEvent($mangaTitle, $chapterNumber, $pageData); + $this->eventDispatcher->dispatch($event, MangaScrapedEvent::NAME); + + return $pageData; + } + + /** + * @throws GuzzleException + */ + private function fetchHtml(string $url): string + { + $client = new Client(); + $response = $client->get($url); + + return (string) $response->getBody(); + } + + /** + * @throws GuzzleException + */ + private function downloadAndSaveImage(string $imageUrl, string $destinationPath): void + { + $client = new Client(); + $response = $client->get($imageUrl); + + file_put_contents($destinationPath, $response->getBody()->getContents()); + } + + /** + * @throws GuzzleException + */ + private function isChapterAvailable(string $chapterUrl, float $chapterNumber): bool + { + $html = $this->fetchHtml($chapterUrl); + $crawler = new Crawler($html); + $nextLink = $crawler->filter('a[title="Suivant"]'); + + if($nextLink->count() === 0){ + return false; + }else{ + $nextUrl = $nextLink->attr('href'); + } + + $routeCollection = new RouteCollection(); + $routeCollection->add('manga_chapter', new Route('/scan-{manga}/{chapter}/{page}')); + $context = new RequestContext('/'); + $matcher = new UrlMatcher($routeCollection, $context); + $path = parse_url($nextUrl, PHP_URL_PATH); + $parameters = $matcher->match($path); + + if((float) $parameters['chapter'] !== $chapterNumber){ + return false; + } + + return true; + } +} diff --git a/src/Service/MangaUpdatesDbProvider.php b/src/Service/MangaUpdatesMetadataProvider.php similarity index 72% rename from src/Service/MangaUpdatesDbProvider.php rename to src/Service/MangaUpdatesMetadataProvider.php index 9ba7965..1cfaff8 100644 --- a/src/Service/MangaUpdatesDbProvider.php +++ b/src/Service/MangaUpdatesMetadataProvider.php @@ -3,20 +3,19 @@ namespace App\Service; use App\Entity\Manga; +use App\Interface\MetadataProviderInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Exception; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; -use Symfony\Component\BrowserKit\HttpBrowser; use Symfony\Component\String\Slugger\SluggerInterface; -use Symfony\Contracts\HttpClient\HttpClientInterface; -class MangaUpdatesDbProvider implements MangaDbProviderInterface +class MangaUpdatesMetadataProvider implements MetadataProviderInterface { private Client $client; - public function __construct(private SluggerInterface $slugger) + public function __construct(private readonly SluggerInterface $slugger) { $this->client = new Client(); } @@ -40,6 +39,9 @@ class MangaUpdatesDbProvider implements MangaDbProviderInterface $results = $this->client->request('POST', 'https://api.mangaupdates.com/v1/series/search', [ 'json' => [ 'search' => $title, + 'licensed' => 'yes', + 'type' => ['Manga'], + 'exclude_genre' => ['Doujinshi', 'Adult', 'Hentai', 'Ecchi', 'Yaoi', 'Yuri', 'Josei', 'Smut', 'Gender Bender'], 'orderby' => 'score', ] ])->withHeader('Authorization', 'Bearer ' . $jwt) @@ -50,13 +52,21 @@ class MangaUpdatesDbProvider implements MangaDbProviderInterface $mangas = []; foreach (json_decode($results, true)['results'] as $record) { $record = $record['record']; + + $genres = []; + foreach ($record['genres'] as $genre) { + $genres[] = $genre['genre']; + } + $mangas[] = (new Manga()) ->setTitle($record['title']) ->setSlug($this->slugger->slug($record['title'])->lower()) ->setDescription($record['description']) ->setImageUrl($record['image']['url']['original']) - ->setGenres($record['genres']) - ->setPublicationYear((int)$record['year']); + ->setGenres($genres) + ->setPublicationYear((int)$record['year']) + ->setRating((float)$record['bayesian_rating']) + ; } return new ArrayCollection($mangas); diff --git a/src/Service/MangadexProvider.php b/src/Service/MangadexProvider.php new file mode 100644 index 0000000..83db533 --- /dev/null +++ b/src/Service/MangadexProvider.php @@ -0,0 +1,123 @@ +client->get('/manga', [ + 'title' => $title, + 'contentRating' => ['safe'], + 'includes' => ['cover_art', 'author'] + ]); + + $mangas = []; + foreach ($results['data'] as $result) { + $mangas[] = (new Manga()) + ->setExternalId($result['id']) + ->setTitle($result['attributes']['title']['en']) + ->setSlug($this->slugger->slug($result['attributes']['title']['en'])->lower()) + ->setDescription($result['attributes']['description']['fr'] ?? $result['attributes']['description']['en'] ?? '') + ->setPublicationYear($result['attributes']['year']) + ; + $tags = []; + foreach($result['attributes']['tags'] as $tag){ + $tags[] = $tag['attributes']['name']['en']; + } + + $mangas[count($mangas) - 1]->setGenres($tags); + + foreach($result['relationships'] as $relationship) { + if($relationship['type'] === 'author') { + $mangas[count($mangas) - 1]->setAuthor($relationship['attributes']['name']); + } + + if($relationship['type'] === 'cover_art') { + $mangas[count($mangas) - 1]->setImageUrl('https://mangadex.org/covers/' . $result['id'] . '/' .$relationship['attributes']['fileName']); + } + } + } + + $test = array_map(fn($manga) => $manga->getExternalId(), $mangas); + + $ratings = $this->client->get('/statistics/manga', [ + 'manga' => $test + ]); + + foreach($mangas as $manga) { + $manga->setRating($ratings['statistics'][$manga->getExternalId()]['rating']['average']); + } + + return new ArrayCollection($mangas); + } + + public function getFeed(Manga $manga): Manga + { + if($manga->getExternalId() === null) { + return $manga; + } + + $chapters = []; + $page = 0; + + do { + $results = $this->getFeedWithPagination($manga->getExternalId(), $page); + if (isset($results['data'])) { + $chapters = array_merge($chapters, $results['data']); + } else { + break; + } + $page++; + } while (count($chapters) < $results['total']); + + foreach($chapters as $result) { + $chapterNumber = (float)$result['attributes']['chapter']; + + // Utilisez la méthode exists de Doctrine pour vérifier si un chapitre avec le même numéro existe déjà + $chapterExists = $manga->getChapters()->exists(function($key, $existingChapter) use ($chapterNumber) { + return $existingChapter->getNumber() === $chapterNumber; + }); + + // Si le chapitre existe déjà, on skip + if ($chapterExists) { + continue; + } + + // Créez et ajoutez le nouveau chapitre + $chapter = new Chapter(); + $chapter->setNumber($chapterNumber) + ->setTitle($result['attributes']['title']) + ->setVolume((int)$result['attributes']['volume'] ?? null); + + $manga->addChapter($chapter); + } + + return $manga; + } + + private function getFeedWithPagination(string $externalId, int $page){ + return $this->client->get('/manga/' . $externalId . '/feed', [ + 'limit' => 500, + 'translatedLanguage' =>['en'], + 'order' => ['chapter' => 'asc'], + 'offset' => $page * 500 + ]); + } +} diff --git a/src/Service/SushiScanProviderService.php b/src/Service/SushiScanProviderService.php index eade353..dccc42a 100644 --- a/src/Service/SushiScanProviderService.php +++ b/src/Service/SushiScanProviderService.php @@ -2,33 +2,72 @@ namespace App\Service; -use Goutte\Client; +use App\Entity\Manga; +use App\Interface\ContentProviderInterface; +use Symfony\Component\BrowserKit\HttpBrowser; +use Symfony\Component\BrowserKit\HttpBrowser as Client; +//use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\HttpClient\HttpClient; -class SushiScanProviderService implements MangaProviderInterface +class SushiScanProviderService { - const PROVIDER_URL = 'https://sushiscan.com/'; - const MANGA_SLUG = '/{manga}/{chapter}/{page}'; - private Client $client; + const PROVIDER_URL = 'https://sushiscan.net/catalogue/'; + const MANGA_SLUG = '/{manga}/{chapter}/{page}'; - public function __construct() - { - $this->client = new Client(); - } + const CONTENT_TYPE = ['volume', 'chapitre']; + private Client $client; - /** - * @return array - */ - public function getMangaList(): array - { - // TODO: Implement getMangaList() method. - } + public function __construct() + { + $httpClient = HttpClient::create(['timeout' => 60]); + $this->client = new HttpBrowser($httpClient); + } - /** - * @param string $mangaSlug - * @return array - */ - public function getChapterList(string $mangaSlug): array - { - // TODO: Implement getChapterList() method. - } -} \ No newline at end of file + public function getAvailableContent(Manga $manga) + { + $url = 'http://flaresolverr:8191/v1'; + $jsonContent = json_encode([ + 'cmd' => 'request.get', + 'url' => self::PROVIDER_URL . $manga->getSlug(), + 'maxTimeout' => 90000, + ]); + + + try{ + $crawler = $this->client->request('POST', $url, [], [], [ + 'HTTP_CONTENT_TYPE' => 'application/json', + ], $jsonContent); + + }catch (\Exception $e) { + dd($e); + } + $contentList = []; + + dd($crawler); + + $crawler->filter('#chapterList ul > li')->each(function (Crawler $node) use (&$contentList) { + dump($node); +// $contentName = $node->text(); +// $contentUrl = $node->attr('href'); +// if ($contentName && $contentUrl) { +// $contentList[] = [ +// 'name' => $contentName, +// 'url' => $contentUrl, +// ]; +// } + }); + + return $contentList; + } + + /** + * @param string $mangaSlug + * @return array + */ + public function getChapterList(string $mangaSlug): array + { + // TODO: Implement getChapterList() method. + } +} diff --git a/src/Twig/Components/AddMangaModalComponent.php b/src/Twig/Components/AddMangaModalComponent.php new file mode 100644 index 0000000..b6633f2 --- /dev/null +++ b/src/Twig/Components/AddMangaModalComponent.php @@ -0,0 +1,27 @@ +manga = $manga; + } + + public function close(): void + { + $this->manga = null; + } +} diff --git a/src/Twig/Components/BootstrapModal.php b/src/Twig/Components/BootstrapModal.php new file mode 100644 index 0000000..331c5da --- /dev/null +++ b/src/Twig/Components/BootstrapModal.php @@ -0,0 +1,11 @@ +mangaSlug; +// $chapter = $this->chapter; +// $manga = $mangaRepository->findOneBy(['slug' => $mangaSlug]); +// $chapter = $chapterRepository->findOneBy(['manga' => $manga, 'number' => $chapter]); + + + return 0; + + } +} diff --git a/src/Twig/Components/MangaSearch.php b/src/Twig/Components/MangaSearch.php new file mode 100644 index 0000000..cfd1ae1 --- /dev/null +++ b/src/Twig/Components/MangaSearch.php @@ -0,0 +1,38 @@ +query === null || $this->query === '') { + return null; + } + + return $this->mangadexProvider->search($this->query); + } +} diff --git a/src/Twig/Components/NewMangaForm.php b/src/Twig/Components/NewMangaForm.php new file mode 100644 index 0000000..d91f531 --- /dev/null +++ b/src/Twig/Components/NewMangaForm.php @@ -0,0 +1,79 @@ +manga = $manga; + $this->mangaData = [ + 'title' => $manga->getTitle(), + 'slug' => $manga->getSlug(), + 'description' => $manga->getDescription(), + 'imageUrl' => $manga->getImageUrl(), + 'status' => $manga->getStatus(), + 'genres' => $manga->getGenres(), + 'author' => $manga->getAuthor(), + 'publicationYear' => $manga->getPublicationYear(), + 'rating' => $manga->getRating(), + 'externalId' => $manga->getExternalId(), + ]; + } + + #[LiveAction] + public function saveManga(EntityManagerInterface $entityManager, MangadexProvider $mangadexProvider): Response + { + $manga = new Manga(); + $manga->setTitle($this->mangaData['title']) + ->setSlug($this->mangaData['slug']) + ->setDescription($this->mangaData['description']) + ->setImageUrl($this->mangaData['imageUrl']) + ->setStatus($this->mangaData['status']) + ->setGenres($this->mangaData['genres']) + ->setAuthor($this->mangaData['author']) + ->setPublicationYear($this->mangaData['publicationYear']) + ->setRating($this->mangaData['rating']) + ->setExternalId($this->mangaData['externalId']); + + $mangadexProvider->getFeed($manga); + try { + foreach ($manga->getChapters() as $chapter) { + $entityManager->persist($chapter); + } + + $entityManager->persist($manga); + $entityManager->flush(); + } catch (\Exception $e) { + if ($e instanceof UniqueConstraintViolationException) { + return new RedirectResponse('/manga/' . $manga->getSlug()); + } + throw $e; + } + + return new RedirectResponse('/manga/' . $manga->getSlug()); + } +} diff --git a/src/Twig/Components/Search.php b/src/Twig/Components/Search.php new file mode 100644 index 0000000..5652231 --- /dev/null +++ b/src/Twig/Components/Search.php @@ -0,0 +1,28 @@ +query ? $this->mangaRepository->findByTitle($this->query) : []; + } +} diff --git a/src/Twig/Components/ToolBarButton.php b/src/Twig/Components/ToolBarButton.php new file mode 100644 index 0000000..45f4edf --- /dev/null +++ b/src/Twig/Components/ToolBarButton.php @@ -0,0 +1,12 @@ + $limit ? substr($value, 0, $limit) . '...' : $value; + } +} diff --git a/src/Twig/Runtime/TruncateExtensionRuntime.php b/src/Twig/Runtime/TruncateExtensionRuntime.php new file mode 100644 index 0000000..20b23f0 --- /dev/null +++ b/src/Twig/Runtime/TruncateExtensionRuntime.php @@ -0,0 +1,18 @@ +⚫️"> {% block stylesheets %} {{ encore_entry_link_tags('app') }} - {{ encore_entry_link_tags('app') }} + {% endblock %} + {% block javascripts %} + {{ encore_entry_script_tags('app') }} {% endblock %} - + +
+
+ +
+ +
+
+
+ {% include 'menu/menu.html.twig' %} -
- - Mangarr - +
+
+ {% block toolbar %} + {% endblock %} +
+
+ {% block body %} + {% endblock %} +
+
+
-
- {% include 'menu/menu.html.twig' %} - {% block body %}{% endblock %} -
-{% block javascripts %} - {{ encore_entry_script_tags('app') }} - {{ encore_entry_script_tags('app') }} -{% endblock %} - diff --git a/templates/bundles/TwigBundle/Exception/error404.html.twig b/templates/bundles/TwigBundle/Exception/error404.html.twig new file mode 100644 index 0000000..45cf13f --- /dev/null +++ b/templates/bundles/TwigBundle/Exception/error404.html.twig @@ -0,0 +1,9 @@ +{# templates/bundles/TwigBundle/Exception/error404.html.twig #} +{% extends 'base.html.twig' %} + +{% block title %}Page non trouvée{% endblock %} + +{% block body %} +

Page non trouvée

+

La page que vous cherchez n'existe pas.

+{% endblock %} diff --git a/templates/components/AddMangaModal.html.twig b/templates/components/AddMangaModal.html.twig new file mode 100644 index 0000000..370c77b --- /dev/null +++ b/templates/components/AddMangaModal.html.twig @@ -0,0 +1,12 @@ +{# templates/components/manga_modal.html.twig #} +
+ +
diff --git a/templates/components/BootstrapModal.html.twig b/templates/components/BootstrapModal.html.twig new file mode 100644 index 0000000..e99311d --- /dev/null +++ b/templates/components/BootstrapModal.html.twig @@ -0,0 +1,33 @@ +
+ +
diff --git a/templates/components/DownloadChapter.html.twig b/templates/components/DownloadChapter.html.twig new file mode 100644 index 0000000..df74fce --- /dev/null +++ b/templates/components/DownloadChapter.html.twig @@ -0,0 +1,5 @@ + + + + +
diff --git a/templates/components/MangaSearch.html.twig b/templates/components/MangaSearch.html.twig new file mode 100644 index 0000000..7b8aeaa --- /dev/null +++ b/templates/components/MangaSearch.html.twig @@ -0,0 +1,65 @@ +{# templates/components/MangaSearch.html.twig #} + +
+
+
+
+ + + + + +
+
+
+ + {% if this.mangas %} +
+ {% for manga in this.mangas %} +
+ + {{ manga.title }} + +
+
+
+
+ {{ manga.title }} + ({{ manga.publicationYear }}) +
+ + + +
+ {% for genre in manga.genres %} + + {{ genre }} + + {% endfor %} +
+
+

{{ manga.description|truncate(250) }}

+
+
+ + + {{ manga.rating }} + +
+
+
+ {{ component('NewMangaForm', {manga: manga, index: loop.index}) }} + {% endfor %} +
+ {% endif %} +
diff --git a/templates/components/NewMangaForm.html.twig b/templates/components/NewMangaForm.html.twig new file mode 100644 index 0000000..9144294 --- /dev/null +++ b/templates/components/NewMangaForm.html.twig @@ -0,0 +1,40 @@ +
+ {% component BootstrapModal with {id: 'mangaModal' ~ index ~ '-' ~ manga.slug } %} + {% block modal_header %} +
+ + +
+ {% endblock %} + {% block modal_body %} +
+
+ {{ manga.title }} +
+

{{ manga.description }}

+

Année de publication: {{ manga.publicationYear }}

+

Genres: {{ manga.genres|join(', ') }}

+

Note: {{ manga.rating }}

+
+
+
+ {% endblock %} + + {% block modal_footer %} + + {% endblock %} + {% endcomponent %} +
diff --git a/templates/components/Search.html.twig b/templates/components/Search.html.twig new file mode 100644 index 0000000..70c1cc5 --- /dev/null +++ b/templates/components/Search.html.twig @@ -0,0 +1,37 @@ + +
+ + +
+ + {% if query %} +
+ +
+ {% endif %} + diff --git a/templates/components/ToolBarButton.html.twig b/templates/components/ToolBarButton.html.twig new file mode 100644 index 0000000..29f9f1f --- /dev/null +++ b/templates/components/ToolBarButton.html.twig @@ -0,0 +1,6 @@ + + + diff --git a/templates/manga/add_new.html.twig b/templates/manga/add_new.html.twig index a5a3daa..c73c410 100644 --- a/templates/manga/add_new.html.twig +++ b/templates/manga/add_new.html.twig @@ -1,80 +1,6 @@ {% extends 'base.html.twig' %} {% block body %} -
-
-
- -
- - - -
-
-
-
-
-
-
- - -
-
+
+ {{ component('MangaSearch', {query: query}) }}
{% endblock %} - -{% block javascripts %} - -{% endblock %} diff --git a/templates/manga/index.html.twig b/templates/manga/index.html.twig index 3a62793..1f371e1 100644 --- a/templates/manga/index.html.twig +++ b/templates/manga/index.html.twig @@ -1,16 +1,42 @@ {% extends 'base.html.twig' %} +{% block toolbar %} +
+
+
+ + +
+ + +
+ +
+
+ +
+ + + +
+
+
+{% endblock %} {% block body %} -
+
{% for manga in mangas %} -
+
- {{ manga.title }} -{# {{ manga.title }}#} + {{ manga.title }}
-

{{ manga.title }}

-

Auteur

-

Ajouter: Jun 2 2024

+
+ {{ manga.title }} + ({{ manga.publicationYear }}) +
+

Added: {{ manga.createdAt|date('M d, Y') }}

{% else %} diff --git a/templates/manga/show_chapters.html.twig b/templates/manga/show_chapters.html.twig index b388c3e..0212e93 100644 --- a/templates/manga/show_chapters.html.twig +++ b/templates/manga/show_chapters.html.twig @@ -1,64 +1,135 @@ {% extends 'base.html.twig' %} +{% block toolbar %} +
+
+
+ + +
+ + +
+ + +
+
+
+{% endblock %} {% block body %} -
-

Chapitres de {{ manga.title }}

- - - -

Scrapper un chapitre

-
- -
- - +
+
+
+
+
+ +
+
+ +

{{ manga.title }}

+
+
+ {{ manga.publicationYear }} + Chapters: {{ manga.chapters.count }} +
+
+ + /media/mangas/{{ manga.title }} ({{ manga.publicationYear }}) + {{ manga.status ?? 'Terminé' }} +
+
+ {% set genre_count = 0 %} + {% for genre in manga.genres %} + {% if genre_count < 5 %} + {{ genre }} + {% set genre_count = genre_count + 1 %} + {% endif %} + {% endfor %} + {% if genre_count == 5 and manga.genres|length > 5 %} + ... + {% endif %} +
+
+
+ + {{ manga.rating|round(2) }} +
+

{{ manga.description|truncate(500) }}

+
+
+
-
- - -
-
- - -
- - +
- {% block javascripts %} - {{ parent() }} - - {% endblock %} + + {% if chapter.pages|length > 0 %} + {{ chapter.title ?? 'No title' }} + {% else %} + {{ chapter.title ?? 'No title'}} + {% endif %} + + + {# #} + + + + {# #} + {# #} + {# #} + + + {% endfor %} + + +
+
+
+ {% endfor %} +
{% endblock %} diff --git a/templates/menu/menu.html.twig b/templates/menu/menu.html.twig index edd3268..25320c3 100644 --- a/templates/menu/menu.html.twig +++ b/templates/menu/menu.html.twig @@ -3,12 +3,15 @@