├── .DS_Store ├── .ddev ├── config.yaml ├── docker-compose.typesense.yaml └── web-build │ └── Dockerfile ├── .dockerignore ├── .env ├── .env.test ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── doc.yml │ ├── docker-publish.yml │ └── tests.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── .phpactor.json ├── .phpunit.cache └── test-results ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── ReadEbook.vue ├── app.js ├── bootstrap.js ├── controllers.json ├── controllers │ ├── assistant-controller.js │ ├── inline-edit-controller.js │ └── search-controller.js ├── login.js ├── read-ebook.js └── styles │ ├── components │ ├── assistant.css │ ├── book.css │ ├── bookDetails.css │ ├── bookGrid.css │ ├── bookReader.css │ ├── bookWithDetails.css │ ├── card.css │ ├── facets.css │ ├── heading.css │ ├── hero.css │ ├── heroProgress.css │ ├── login.css │ ├── menuBlock.css │ ├── rating.css │ ├── sidebar.css │ └── table.css │ ├── global.scss │ ├── layout.css │ ├── overrides.css │ └── variables.css ├── backups └── .gitkeep ├── bin ├── console └── phpunit ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── packages │ ├── biblioverse_typesense.yaml │ ├── cache.yaml │ ├── csrf.yaml │ ├── debug.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── http_discovery.yaml │ ├── knp_menu.yaml │ ├── knp_paginator.yaml │ ├── liip_imagine.yaml │ ├── mailer.yaml │ ├── messenger.yaml │ ├── monolog.yaml │ ├── notifier.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── translation.yaml │ ├── twig.yaml │ ├── twig_component.yaml │ ├── validator.yaml │ ├── web_profiler.yaml │ └── webpack_encore.yaml ├── preload.php ├── routes.yaml ├── routes │ ├── framework.yaml │ ├── liip_imagine.yaml │ ├── ux_autocomplete.yaml │ ├── ux_live_component.yaml │ └── web_profiler.yaml └── services.yaml ├── doc ├── .gitignore ├── astro.config.mjs ├── package-lock.json ├── package.json ├── public │ └── favicon.svg ├── src │ ├── assets │ │ ├── natural_language.gif │ │ ├── summary.png │ │ └── tags.png │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── Troubleshooting │ │ │ ├── documentation.md │ │ │ └── github.md │ │ │ ├── demo.mdx │ │ │ ├── guides │ │ │ ├── Administrator │ │ │ │ ├── adding-books.mdx │ │ │ │ ├── adding-users.mdx │ │ │ │ ├── ai-features-configuration.mdx │ │ │ │ ├── commands.md │ │ │ │ ├── deleting-books.mdx │ │ │ │ ├── edit-book.md │ │ │ │ ├── filesystem.md │ │ │ │ ├── instance-configuration.mdx │ │ │ │ └── verified.md │ │ │ ├── Developer │ │ │ │ ├── themes.mdx │ │ │ │ └── translations.mdx │ │ │ └── User │ │ │ │ ├── OPDS.mdx │ │ │ │ ├── homepage.md │ │ │ │ ├── interactions.md │ │ │ │ ├── searching.md │ │ │ │ ├── shelf.md │ │ │ │ ├── sync-kobo.mdx │ │ │ │ └── update-settings.md │ │ │ ├── index.mdx │ │ │ └── installing │ │ │ ├── 1-helm.mdx │ │ │ ├── 1-initial-setup.mdx │ │ │ ├── 2-new-versions.mdx │ │ │ ├── dotenv-config.md │ │ │ └── unraid.md │ ├── custom.css │ ├── env.d.ts │ └── files │ │ ├── docker-compose.yml │ │ └── helm-values.yml └── tsconfig.json ├── docker-compose.test.yml ├── docker-compose.yml ├── justfile ├── migrations ├── .gitignore ├── AbstractMigration.php ├── Version20240101000000.php ├── Version20240112163633.php ├── Version20240121151215.php ├── Version20240121162659.php ├── Version20240219181604.php ├── Version20240505101909.php ├── Version20240510173645.php ├── Version20240512162606.php ├── Version20240513152414.php ├── Version20240531124659.php ├── Version20240531125758.php ├── Version20240714144826.php ├── Version20240721080644.php ├── Version20240812195147.php ├── Version20241121135658.php ├── Version20241121142239.php ├── Version20241129185216.php ├── Version20241208200901.php ├── Version20241210143123.php ├── Version20241218154423.php ├── Version20241218154713.php ├── Version20241223182304.php ├── Version20250101174217.php ├── Version20250106061005.php ├── Version20250106090320.php ├── Version20250106091638.php ├── Version20250106094820.php ├── Version20250123184640.php ├── Version20250128213230.php ├── Version20250128214238.php ├── Version20250130192538.php ├── Version20250131184632.php ├── Version20250201163836.php ├── Version20250202073734.php ├── Version20250202150749.php └── Version20250213182540.php ├── package-lock.json ├── package.json ├── phpstan.neon ├── phpunit.xml.dist ├── public ├── .htaccess ├── images │ ├── .gitkeep │ └── blank.jpg ├── index.php └── media │ └── .gitkeep ├── rector.php ├── renovate.json ├── src ├── Ai │ ├── Communicator │ │ ├── AbstractCommunicator.php │ │ ├── AiAction.php │ │ ├── AiChatInterface.php │ │ ├── AiCommunicatorInterface.php │ │ ├── CommunicatorDefiner.php │ │ ├── GeminiCommunicator.php │ │ ├── OllamaCommunicator.php │ │ ├── OpenAiCommunicator.php │ │ └── PerplexicaCommunicator.php │ ├── Context │ │ ├── ContextBuilder.php │ │ ├── ContextBuildingInterface.php │ │ └── PerplexicaContextBuilder.php │ ├── Message.php │ └── Prompt │ │ ├── AbstractBookPrompt.php │ │ ├── BookPromptInterface.php │ │ ├── PromptFactory.php │ │ ├── SearchHintPrompt.php │ │ ├── SummaryPrompt.php │ │ └── TagPrompt.php ├── Command │ ├── BackupDbCommand.php │ ├── BooksAiCommand.php │ ├── BooksCheckCommand.php │ ├── BooksExtractCoverCommand.php │ ├── BooksRelocateCommand.php │ ├── BooksScanCommand.php │ ├── CreateUserCommand.php │ └── TranslationRetrieveFromProfilerCommand.php ├── Config │ └── ConfigValue.php ├── Controller │ ├── .gitignore │ ├── AbstractController.php │ ├── AiModelController.php │ ├── AuthorController.php │ ├── AutocompleteGroupController.php │ ├── BookController.php │ ├── DefaultController.php │ ├── GroupController.php │ ├── InstanceConfigurationController.php │ ├── Kobo │ │ ├── AbstractKoboController.php │ │ ├── Api │ │ │ ├── ApiEndpointController.php │ │ │ ├── ImageController.php │ │ │ ├── V1 │ │ │ │ ├── AnalyticsController.php │ │ │ │ ├── DownloadController.php │ │ │ │ ├── GenericController.php │ │ │ │ ├── InitializationController.php │ │ │ │ ├── Library │ │ │ │ │ ├── LibraryController.php │ │ │ │ │ ├── MetadataController.php │ │ │ │ │ └── StateController.php │ │ │ │ ├── ProductsController.php │ │ │ │ └── TagController.php │ │ │ └── V3 │ │ │ │ └── ContentController.php │ │ ├── KoboDeviceController.php │ │ └── KoboNextReadController.json │ ├── LoginController.php │ ├── OPDS │ │ └── OpdsController.php │ ├── OpdsAccessController.php │ ├── SerieController.php │ ├── ShelfController.php │ ├── ShelfCrudController.php │ └── UserController.php ├── DataFixtures │ ├── BookFixture.php │ ├── KoboFixture.php │ ├── OpdsAccessFixture.php │ ├── ShelfFixture.php │ ├── ShelfKoboFixture.php │ ├── UserFixture.php │ └── books.yaml ├── Entity │ ├── .gitignore │ ├── AiModel.php │ ├── Book.php │ ├── BookInteraction.php │ ├── BookmarkUser.php │ ├── InstanceConfiguration.php │ ├── KoboDevice.php │ ├── KoboSyncedBook.php │ ├── OpdsAccess.php │ ├── RandomGeneratorTrait.php │ ├── Shelf.php │ ├── User.php │ └── UuidGeneratorTrait.php ├── Enum │ ├── AgeCategory.php │ ├── AiMessageRole.php │ ├── ReadStatus.php │ └── ReadingList.php ├── EventListener │ ├── LanguageListener.php │ └── LoginListener.php ├── EventSubscriber │ └── KoboLogRequestSubscriber.php ├── Exception │ ├── BookExtractionException.php │ └── BookFileNotFound.php ├── Form │ ├── AiConfigurationType.php │ ├── AiModelType.php │ ├── BookType.php │ ├── InlineInteractionType.php │ ├── InstanceConfigurationType.php │ ├── KoboLastSyncTokenType.php │ ├── KoboType.php │ ├── LabelTranslationFormExtension.php │ ├── ProfileType.php │ ├── ShelfType.php │ └── UserType.php ├── Kernel.php ├── Kobo │ ├── BookDownloadInfo.php │ ├── DownloadHelper.php │ ├── ImageProcessor │ │ └── CoverTransformer.php │ ├── Kepubify │ │ ├── KebpubifyCachedData.php │ │ ├── KepubifyConversionFailed.php │ │ ├── KepubifyEnabler.php │ │ ├── KepubifyMessage.php │ │ └── KepubifyMessageHandler.php │ ├── LogProcessor │ │ └── KoboContextProcessor.php │ ├── ParamConverter │ │ ├── KoboParamConverter.php │ │ └── SyncTokenParamConverter.php │ ├── Proxy │ │ ├── KoboHeaderFilterTrait.php │ │ ├── KoboProxyConfiguration.json │ │ ├── KoboProxyConfiguration.php │ │ ├── KoboProxyListener.php │ │ ├── KoboProxyLogger.php │ │ ├── KoboProxyLoggerFactory.php │ │ └── KoboStoreProxy.php │ ├── Request │ │ ├── Bookmark.php │ │ ├── ReadingState.php │ │ ├── ReadingStateLocation.php │ │ ├── ReadingStateStatistics.php │ │ ├── ReadingStateStatusInfo.php │ │ ├── ReadingStates.php │ │ ├── TagDeleteRequest.php │ │ └── TagDeleteRequestItem.php │ ├── Response │ │ ├── MetadataResponseService.php │ │ ├── ReadingStateResponse.php │ │ ├── ReadingStateResponseFactory.php │ │ ├── StateResponse.php │ │ ├── SyncResponse.php │ │ ├── SyncResponseFactory.php │ │ └── SyncResponseHelper.php │ ├── Serializer │ │ └── KoboNameConverter.php │ ├── SyncToken.php │ ├── SyncTokenParser.php │ └── UpstreamSyncMerger.php ├── Menu │ └── MenuBuilder.php ├── OPDS │ └── Opds.php ├── Repository │ ├── .gitignore │ ├── AiModelRepository.php │ ├── BookInteractionRepository.php │ ├── BookRepository.php │ ├── BookmarkUserRepository.php │ ├── InstanceConfigurationRepository.php │ ├── KoboDeviceRepository.php │ ├── KoboSyncedBookRepository.php │ ├── OpdsAccessRepository.php │ ├── ShelfRepository.php │ └── UserRepository.php ├── Security │ ├── Badge │ │ └── KoboDeviceBadge.php │ ├── KoboAccessTokenAuthenticator.php │ ├── KoboAccessTokenHandlerInterface.php │ ├── KoboTokenExtractor.php │ ├── KoboTokenHandler.php │ ├── OpdsTokenExtractor.php │ ├── OpdsTokenHandler.php │ ├── Token │ │ └── PostAuthenticationTokenWithKoboDevice.php │ └── Voter │ │ ├── AiFeaturesVoter.php │ │ ├── BookInteractionVoter.php │ │ ├── BookVoter.php │ │ ├── KoboDeviceVoter.php │ │ └── RelocationVoter.php ├── Service │ ├── AccessControlSubscriber.php │ ├── BookArchiver.php │ ├── BookFileSystemManager.php │ ├── BookFileSystemManagerInterface.php │ ├── BookInteractionService.php │ ├── BookManager.php │ ├── BookProgressionService.php │ ├── ConnectionKeepAliveSubscriber.php │ ├── FilteredBookUrlGenerator.php │ ├── KoboSyncTokenExtractor.php │ ├── Search │ │ └── SearchHelper.php │ ├── ShelfManager.php │ └── ThemeSelector.php └── Twig │ ├── Components │ ├── AddNewShelf.php │ ├── Assistant.php │ ├── BootstrapModal.php │ ├── FieldGuesser.php │ ├── InlineEditGroup.php │ ├── InlineEditInteraction.php │ ├── InlineEditMultiple.php │ ├── InlineEditVerified.php │ ├── Search.php │ └── UploadBookPicture.php │ ├── Extension │ └── ThemeExtension.php │ ├── FilteredBookUrl.php │ ├── Runtime │ └── ThemeExtensionRuntime.php │ └── UniqueIdExtension.php ├── symfony.lock ├── templates ├── ai_model │ ├── _form.html.twig │ ├── edit.html.twig │ ├── index.html.twig │ ├── new.html.twig │ └── show.html.twig ├── author │ └── detail.html.twig ├── bare.html.twig ├── base.html.twig ├── book │ ├── _book-row-empty.html.twig │ ├── _book-row.html.twig │ ├── _cover.html.twig │ ├── _cover_empty.html.twig │ ├── _empty.html.twig │ ├── _knp_minimal_pagination.html.twig │ ├── _teaser.html.twig │ ├── consume.html.twig │ ├── index.html.twig │ ├── reader-files-epub.html.twig │ ├── reader-files.html.twig │ └── upload.html.twig ├── bundles │ └── TwigBundle │ │ └── Exception │ │ └── error.html.twig ├── components │ ├── AddNewShelf.html.twig │ ├── Assistant.html.twig │ ├── BootstrapModal.html.twig │ ├── FieldGuesser.html.twig │ ├── InlineEditGroup.html.twig │ ├── InlineEditInteraction.html.twig │ ├── InlineEditMultiple.html.twig │ ├── InlineEditVerified.html.twig │ ├── Search.html.twig │ ├── UploadBookPicture.html.twig │ ├── _facet.html.twig │ └── _rating.html.twig ├── default │ ├── dashboard.html.twig │ ├── index.html.twig │ ├── notverified.html.twig │ ├── readingList.html.twig │ └── timeline.html.twig ├── group │ └── index.html.twig ├── instance_configuration │ ├── _form.html.twig │ ├── edit.html.twig │ └── index.html.twig ├── kobodevice_user │ ├── _delete_form.html.twig │ ├── _form.html.twig │ ├── _form_theme.html.twig │ ├── edit.html.twig │ ├── index.html.twig │ ├── instructions.html.twig │ ├── logs.html.twig │ └── new.html.twig ├── login │ └── login.html.twig ├── main_menu.html.twig ├── serie │ └── detail.html.twig ├── shelf │ └── index.html.twig ├── shelf_crud │ ├── _form.html.twig │ ├── edit.html.twig │ └── index.html.twig ├── themes │ └── dark │ │ └── bare.html.twig └── user │ ├── _delete_form.html.twig │ ├── _form.html.twig │ ├── edit.html.twig │ ├── index.html.twig │ ├── new.html.twig │ └── profile.html.twig ├── tests ├── Ai │ └── AiModelControllerTest.php ├── Command │ └── CreateUserCommandTest.php ├── Component │ └── SearchComponentTest.php ├── Contraints │ ├── ArrayHasNestedKey.php │ ├── AssertHasDownloadWithFormat.php │ ├── JSONContainKeys.php │ └── JSONIsValidSyncResponse.php ├── Controller │ ├── Kobo │ │ ├── Api │ │ │ ├── V1 │ │ │ │ ├── AnalyticsControllerTest.php │ │ │ │ ├── DownloadControllerTest.php │ │ │ │ ├── GenericControllerTest.php │ │ │ │ ├── InitializationControllerTest.json │ │ │ │ ├── InitializationControllerTest.php │ │ │ │ ├── Library │ │ │ │ │ ├── MetadataControllerTest.php │ │ │ │ │ ├── StateControllerTest.php │ │ │ │ │ └── SyncControllerTest.php │ │ │ │ ├── ProductsControllerTest.php │ │ │ │ └── TagControllerTest.php │ │ │ └── V3 │ │ │ │ └── ContentControllerTest.php │ │ ├── KoboControllerTestCase.php │ │ └── KoboImageControllerTest.php │ └── Opds │ │ ├── AbstractOpdsTestController.php │ │ └── OpdsAccessControllerTest.php ├── DynamicShelfTest.php ├── Entity │ ├── BookTest.php │ └── SyncedBookTest.php ├── FileSystemManagerForTests.php ├── GedmoTest.php ├── InstanceConfigurationControllerTest.php ├── Kobo │ ├── DownloadHelperTest.php │ ├── Proxy │ │ └── KoboProxyConfigurationTest.php │ ├── Request │ │ ├── ReadingStateDeserializeTest.json │ │ └── ReadingStateDeserializeTest.php │ ├── Response │ │ └── SyncResponseHelperTest.php │ └── SyncTokenTest.php ├── LoginTest.php ├── Mock │ └── AbstractApiMock.php ├── Resources │ ├── books │ │ ├── fake-AChristmasCarol.epub │ │ ├── fake-AnnaKarenina.epub │ │ ├── fake-BrothersKaramazov.epub │ │ ├── fake-CountOfMonteCristo.epub │ │ ├── fake-CrimeAndPunishment.epub │ │ ├── fake-DivineComedy.epub │ │ ├── fake-DonQuixote.epub │ │ ├── fake-DorianGray.epub │ │ ├── fake-Dracula.epub │ │ ├── fake-Frankenstein.epub │ │ ├── fake-GreatExpectations.epub │ │ ├── fake-JaneEyre.epub │ │ ├── fake-LesMiserables.epub │ │ ├── fake-MobyDick.epub │ │ ├── fake-PrideAndPrejudice.epub │ │ ├── fake-SherlockHolmes.epub │ │ ├── fake-TaleOfTwoCities.epub │ │ ├── fake-TheJungleBook.epub │ │ ├── fake-WarAndPeace.epub │ │ ├── fake-WutheringHeights.epub │ │ └── real-TheOdysses.epub │ └── covers │ │ └── TheOdysses.jpg ├── SampleTest.php ├── Service │ ├── BookArchiverTest.php │ └── BookProgressionServiceTest.php ├── SmokeTest.php ├── TestCaseHelperTrait.php ├── TestClock.php ├── Translations │ └── TranslationExtractionTest.php ├── bootstrap.php └── coverage │ └── .gitkeep ├── translations ├── .gitignore ├── AutocompleteBundle.en.yaml ├── AutocompleteBundle.fr.yaml ├── KnpPaginatorBundle.en.yaml ├── KnpPaginatorBundle.fr.yaml ├── messages+intl-icu.en.yaml ├── messages+intl-icu.fr.yaml ├── security.en.yaml ├── security.fr.yaml ├── validators.en.yaml └── validators.fr.yaml ├── upgrade.sh └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/.DS_Store -------------------------------------------------------------------------------- /.ddev/docker-compose.typesense.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | typesense: 3 | image: typesense/typesense:0.26.0.rc46 4 | restart: on-failure 5 | ports: 6 | - 8983 7 | - "8108:8108" 8 | volumes: 9 | - searchdata:/data 10 | command: '--data-dir /data --api-key=xyz --enable-cors' 11 | 12 | volumes: 13 | searchdata: -------------------------------------------------------------------------------- /.ddev/web-build/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | ENV COMPOSER_ALLOW_SUPERUSER 1 3 | ENV COMPOSER_HOME /home/.composer 4 | RUN mkdir -p /home/.composer 5 | RUN printf "deb http://http.us.debian.org/debian stable main contrib non-free" > /etc/apt/sources.list.d/nonfree.list 6 | RUN apt-get update 7 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y \ 8 | p7zip-full \ 9 | build-essential \ 10 | unrar && rm -rf /var/lib/apt/lists/* 11 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots 7 | 8 | # Root access needed to create the new TEST database 9 | 10 | DATABASE_URL="mysql://root:biblioteca@db:3306/biblioteca?serverVersion=10.10.0-MariaDB&charset=utf8" 11 | 12 | TYPESENSE_URL=http://typesense:8108 13 | TYPESENSE_KEY=xyz 14 | 15 | # Disable the proxy for the test env 16 | KOBO_PROXY_USE_EVERYWHERE=0 17 | KOBO_PROXY_ENABLED=0 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed changes 2 | 3 | Explain your PR here 4 | 5 | 6 | ## Checklist 7 | - [ ] Tests are passing 8 | - [ ] New tests have been written 9 | - [ ] Documentation has been updated 10 | - [ ] Translations have been updated 11 | - [ ] Breaking changes have been avoided or documented 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/doc.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - migrate-documentation 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | node: [20] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@master 21 | 22 | - name: Setup node env 23 | uses: actions/setup-node@v4.4.0 24 | with: 25 | node-version: ${{ matrix.node }} 26 | 27 | - name: Install dependencies 28 | run: npm ci 29 | working-directory: ./doc 30 | 31 | - name: Generate 32 | run: npm run build 33 | working-directory: ./doc 34 | 35 | - name: Deploy 36 | uses: peaceiris/actions-gh-pages@v4 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | publish_dir: ./doc/dist -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__.'/src') 8 | ->in(__DIR__.'/tests') 9 | ->in(__DIR__.'/migrations') 10 | ; 11 | 12 | $config = new PhpCsFixer\Config('Biblioteca'); 13 | $config->setLineEnding(PHP_EOL); 14 | $config->setFinder($finder); 15 | $config->setRiskyAllowed(true); 16 | $config->setRules([ 17 | '@Symfony' => true, 18 | 'increment_style' => false, 19 | 'logical_operators' => true, 20 | 'no_superfluous_phpdoc_tags' => false, 21 | 'phpdoc_align' => [ 22 | 'align' => 'left', 23 | ], 24 | 'phpdoc_separation' => false, 25 | 'phpdoc_summary' => false, 26 | 'visibility_required' => [ 27 | 'elements' => ['property', 'method', 'const'], 28 | ], 29 | 'method_argument_space' => [ 30 | 'on_multiline' => 'ensure_fully_multiline', 31 | ], 32 | 'yoda_style' => false, 33 | ]); 34 | 35 | return $config; 36 | -------------------------------------------------------------------------------- /.phpactor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "/phpactor.schema.json", 3 | "language_server_phpstan.enabled": true, 4 | "language_server_php_cs_fixer.enabled": true, 5 | "symfony.enabled": true, 6 | "indexer.exclude_patterns": [ 7 | "/vendor/**/Tests/**/*", 8 | "/vendor/**/tests/**/*", 9 | "/var/cache/**/*", 10 | "/vendor/composer/**/*" 11 | ] 12 | } -------------------------------------------------------------------------------- /.phpunit.cache/test-results: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":{"App\\Tests\\GedmoTest::testTimestamp":8,"App\\Tests\\SampleTest::testSample":8},"times":{"App\\Tests\\SampleTest::testSample":0,"App\\Tests\\GedmoTest::testTimestamp":0.133}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Biblioteca 2 | 3 | Biblioteca is a web application made to manage large ebook libraries and is developed aiming to help you to have consistent and well 4 | classified libraries. 5 | 6 | 7 | ## Documentation 8 | [Read the full documentation on the dedicated website](https://biblioverse.github.io/biblioteca/) 9 | 10 | -------------------------------------------------------------------------------- /assets/app.js: -------------------------------------------------------------------------------- 1 | import './bootstrap.js'; 2 | import './styles/global.scss'; 3 | import * as bootstrap from 'bootstrap' 4 | 5 | 6 | window.addEventListener('manager:flush', () => { 7 | setTimeout(function (){location.reload()}, 500) 8 | }); 9 | 10 | const toastElList = document.querySelectorAll('.toast') 11 | const toastList = [...toastElList].map(toastEl => new bootstrap.Toast(toastEl).show()) 12 | 13 | const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') 14 | const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) -------------------------------------------------------------------------------- /assets/bootstrap.js: -------------------------------------------------------------------------------- 1 | import { startStimulusApp } from '@symfony/stimulus-bridge'; 2 | 3 | // Registers Stimulus controllers from controllers.json and in the controllers/ directory 4 | export const app = startStimulusApp(require.context( 5 | '@symfony/stimulus-bridge/lazy-controller-loader!./controllers', 6 | true, 7 | /\.[jt]sx?$/ 8 | )); 9 | // register any custom, 3rd party controllers here 10 | // app.register('some_controller_name', SomeImportedController); 11 | -------------------------------------------------------------------------------- /assets/controllers.json: -------------------------------------------------------------------------------- 1 | { 2 | "controllers": { 3 | "@symfony/ux-autocomplete": { 4 | "autocomplete": { 5 | "enabled": true, 6 | "fetch": "eager", 7 | "autoimport": { 8 | "tom-select/dist/css/tom-select.default.css": false, 9 | "tom-select/dist/css/tom-select.bootstrap4.css": false, 10 | "tom-select/dist/css/tom-select.bootstrap5.css": true 11 | } 12 | } 13 | }, 14 | "@symfony/ux-live-component": { 15 | "live": { 16 | "enabled": true, 17 | "fetch": "eager", 18 | "autoimport": { 19 | "@symfony/ux-live-component/dist/live.min.css": true 20 | } 21 | } 22 | } 23 | }, 24 | "entrypoints": [] 25 | } 26 | -------------------------------------------------------------------------------- /assets/controllers/assistant-controller.js: -------------------------------------------------------------------------------- 1 | import {Controller} from '@hotwired/stimulus'; 2 | import {getComponent} from '@symfony/ux-live-component'; 3 | 4 | export default class extends Controller { 5 | async initialize() { 6 | this.component = await getComponent(this.element); 7 | 8 | window.removeEventListener('select:clear', this.reinitializeTomSelect); 9 | window.addEventListener('select:clear', this.reinitializeTomSelect); 10 | 11 | } 12 | 13 | reinitializeTomSelect(event) { 14 | console.log(event.detail.field); 15 | const selects = document.querySelectorAll(`#book-assistant-${event.detail.book} .tomselected`); 16 | selects.forEach(select => { 17 | if (select.tomselect && select.name.includes(event.detail.field)) { 18 | select.tomselect.sync() 19 | } 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /assets/controllers/inline-edit-controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus'; 2 | import { getComponent } from '@symfony/ux-live-component'; 3 | 4 | export default class extends Controller { 5 | async initialize() { 6 | this.component = await getComponent(this.element); 7 | 8 | this.component.on('render:finished', (component) => { 9 | 10 | if(document.querySelector('.alert-remove')) { 11 | setTimeout(function () { 12 | location.reload(); 13 | }, 300); 14 | } 15 | 16 | }); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /assets/controllers/search-controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus'; 2 | import { getComponent } from '@symfony/ux-live-component'; 3 | 4 | export default class extends Controller { 5 | static targets = [ "search", "query", "suggestions" ] 6 | async initialize() { 7 | this.component = await getComponent(this.element); 8 | 9 | } 10 | 11 | connect() { 12 | } 13 | 14 | redirect() { 15 | const path = window.location.pathname 16 | if(!path.includes('/all')) { 17 | document.getElementById('js-main').innerHTML = '' 18 | 19 | history.pushState({}, '', '/all') 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /assets/read-ebook.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import ReadEBook from './ReadEbook.vue' 3 | 4 | document.addEventListener('DOMContentLoaded', () => { 5 | const mountId = 'vue-book-reader'; 6 | const file = document.getElementById(mountId).getAttribute('data-file') 7 | const css = document.getElementById(mountId).getAttribute('data-css') 8 | const bgColor = document.getElementById(mountId).getAttribute('data-background-color') 9 | const percent = document.getElementById(mountId).getAttribute('data-percent') 10 | const progressionUrl = document.getElementById(mountId).getAttribute('data-progression-url') 11 | const backUrl = document.getElementById(mountId).getAttribute('data-back-url') 12 | createApp(ReadEBook, { 13 | file: file, 14 | css: css, 15 | bgColor: bgColor, 16 | percent: percent, 17 | progressionUrl: progressionUrl, 18 | backUrl: backUrl 19 | }).mount(`#${mountId}`); 20 | }); 21 | -------------------------------------------------------------------------------- /assets/styles/components/assistant.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 768px) { 2 | 3 | .Assistant { 4 | display: grid; 5 | grid-template-columns: 15% auto 30%; 6 | } 7 | 8 | .Assistant__form { 9 | max-height: calc(75vh - 32px); 10 | overflow-y: auto; 11 | padding: 0 1rem; 12 | } 13 | 14 | .Assistant__chat { 15 | max-height: calc(75vh - 32px); 16 | overflow-y: auto; 17 | } 18 | } -------------------------------------------------------------------------------- /assets/styles/components/bookDetails.css: -------------------------------------------------------------------------------- 1 | .BookDetails { 2 | display: grid; 3 | gap: var(--space--md); 4 | grid-template-areas: "actions" "summary"; 5 | } 6 | 7 | .BookDetails--withInfo { 8 | grid-template-areas: "actions" "summary" "info"; 9 | } 10 | 11 | @media (min-width: 768px) { 12 | .BookDetails--withInfo { 13 | grid-template-areas: "summary summary" "info actions"; 14 | grid-template-columns: 1fr 1fr; 15 | } 16 | } 17 | 18 | @media (min-width: 1000px) { 19 | .BookDetails { 20 | grid-template-areas: "summary actions"; 21 | grid-template-columns: 4fr 2fr; 22 | } 23 | 24 | .BookDetails--withInfo { 25 | grid-template-areas: "summary info actions"; 26 | grid-template-columns: 3fr 2fr 2fr; 27 | } 28 | } 29 | 30 | .BookDetails__actions { 31 | grid-area: actions; 32 | } 33 | 34 | .BookDetails__summary { 35 | grid-area: summary; 36 | } 37 | 38 | .BookDetails__info { 39 | grid-area: info; 40 | } 41 | 42 | 43 | /*.BookDetails__item {}*/ 44 | 45 | .BookDetails__item__title { 46 | margin-bottom: var(--space--sm) !important; 47 | } 48 | 49 | .BookDetails__item__content { 50 | list-style-type: none; 51 | padding-left: 1rem; 52 | margin: 0; 53 | } 54 | 55 | * + .BookDetails__item { 56 | margin-top: var(--space--md); 57 | } -------------------------------------------------------------------------------- /assets/styles/components/bookGrid.css: -------------------------------------------------------------------------------- 1 | .bookGrid { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); 4 | gap: var(--space--lg); 5 | } 6 | 7 | .bookGrid--large { 8 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 9 | gap: var(--space--xl); 10 | } 11 | -------------------------------------------------------------------------------- /assets/styles/components/bookReader.css: -------------------------------------------------------------------------------- 1 | .vue-book-reader { 2 | width: 100%; 3 | } 4 | 5 | .vue-book-reader .flex { 6 | display: flex; 7 | } 8 | 9 | .vue-book-reader .flex button { 10 | display: block; 11 | } 12 | -------------------------------------------------------------------------------- /assets/styles/components/bookWithDetails.css: -------------------------------------------------------------------------------- 1 | .BooksWithDetails { 2 | display: grid; 3 | grid-template-areas: "info" "content"; 4 | gap: var(--space--md); 5 | } 6 | 7 | .BooksWithDetails__content { 8 | grid-area: content; 9 | } 10 | 11 | .BooksWithDetails__info { 12 | grid-area: info; 13 | } 14 | 15 | 16 | @media (min-width: 768px) { 17 | .BooksWithDetails { 18 | grid-template-columns: auto minmax(30%, 300px); 19 | grid-template-areas: "content info"; 20 | } 21 | } -------------------------------------------------------------------------------- /assets/styles/components/card.css: -------------------------------------------------------------------------------- 1 | .Card { 2 | background: var(--color-main-03); 3 | border-radius: var(--border-radius--sm); 4 | } 5 | 6 | .Card__title { 7 | padding: var(--space--xl); 8 | padding-bottom: var(--space--sm); 9 | } 10 | 11 | .Card__content { 12 | padding: var(--space--xl); 13 | } 14 | 15 | * + .Card__content { 16 | padding-top: var(--space--sm); 17 | } 18 | -------------------------------------------------------------------------------- /assets/styles/components/facets.css: -------------------------------------------------------------------------------- 1 | .facets { 2 | display: grid; 3 | gap: var(--space--xs); 4 | margin-bottom: var(--space--lg); 5 | } 6 | 7 | .facets__item { 8 | width: 100%; 9 | display: flex; 10 | justify-content: space-between; 11 | gap: var(--space--sm); 12 | font-size: var(--font-size--sm); 13 | 14 | border-width: 0; 15 | background: transparent; 16 | border-radius: var(--border-radius--sm); 17 | padding: var(--space--xs); 18 | line-height: 1.5; 19 | text-align: left; 20 | } 21 | 22 | .facets__item--active { 23 | background: var(--bs-list-group-active-bg); 24 | color: var(--bs-list-group-active-color); 25 | } 26 | -------------------------------------------------------------------------------- /assets/styles/components/heading.css: -------------------------------------------------------------------------------- 1 | .Heading { 2 | margin: 0; 3 | font-size: 1.25rem; 4 | font-weight: 500; 5 | line-height: 1.2em; 6 | } 7 | 8 | .Heading--sm { 9 | font-size: 1rem; 10 | } 11 | 12 | .Heading--lg { 13 | font-size: 1.5rem; 14 | } 15 | 16 | .Heading--xl { 17 | font-size: 1.75rem; 18 | } 19 | 20 | .Heading--2xl { 21 | font-size: 2.5rem; 22 | } 23 | -------------------------------------------------------------------------------- /assets/styles/components/heroProgress.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --progress__track--background: var(--color-main-02); 3 | --progress__track--height: 1em; 4 | --progress__track--border-radius: 4px; 5 | --progress__bar--background: var(--color-main-01); 6 | } 7 | 8 | .heroProgress { 9 | position: absolute; 10 | bottom: -4px; 11 | left: 0; 12 | right: 0; 13 | } 14 | 15 | .heroProgress__bar { 16 | background: var(--progress__track--background); 17 | height: 4px; 18 | } 19 | 20 | .heroProgress__bar__progress { 21 | background: var(--progress__bar--background); 22 | height: 4px; 23 | } 24 | -------------------------------------------------------------------------------- /assets/styles/components/login.css: -------------------------------------------------------------------------------- 1 | .LoginMasonry { 2 | position: absolute; 3 | inset: 0; 4 | overflow: hidden; 5 | 6 | justify-content: space-between; 7 | overflow: hidden; 8 | } 9 | 10 | .LoginMasonry__inner { 11 | width: 100%; 12 | height: 100%; 13 | transform: skew(-0.06turn, 18deg) scale(1.5); /**/ 14 | display: flex; 15 | } 16 | 17 | .LoginMasonry__column { 18 | width: 100%; 19 | flex: 1 1 0%; 20 | } 21 | 22 | .LoginMasonry__column__content { 23 | transition-duration: 100ms; 24 | transition-timing-function: linear; 25 | transition-property: transform; 26 | width: 100%; 27 | } 28 | 29 | .LoginMasonry__item { 30 | width: 100%; 31 | aspect-ratio: 0.75; 32 | overflow: hidden; 33 | } 34 | 35 | .LoginMasonry__item canvas { 36 | width: 100%; 37 | height: 100%; 38 | } 39 | 40 | .LoginOverlay { 41 | position: absolute; 42 | inset: 0; 43 | background: rgba(255, 255, 255, 0.5); 44 | backdrop-filter: blur(12px); 45 | } 46 | 47 | .LoginCardContainer { 48 | position: absolute; 49 | inset: 0; 50 | display: flex; 51 | justify-content: center; 52 | align-items: center; 53 | } 54 | 55 | .LoginCard { 56 | background-color: rgb(255 255 255 / 0.9); 57 | border-radius: 0.5rem; 58 | box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 59 | } 60 | -------------------------------------------------------------------------------- /assets/styles/components/menuBlock.css: -------------------------------------------------------------------------------- 1 | .Menu { 2 | padding-inline: var(--space--sm); 3 | margin-block-end: var(--space--md); 4 | } 5 | 6 | .MenuLink { 7 | display: grid; 8 | grid-template-columns: 1rem auto; 9 | gap: var(--space--sm); 10 | margin-left: 0.5rem; 11 | 12 | transition: 13 | color 0.3s ease-in-out, 14 | background-color 0.3s ease-in-out, 15 | border-color 0.3s ease-in-out; 16 | 17 | text-decoration: none; 18 | align-items: center; 19 | color: var(--bs-nav-link-color); 20 | font-size: var(--font-size--sm); 21 | padding: var(--space--xs) var(--space--sm); 22 | border-radius: var(--border-radius--sm); 23 | } 24 | 25 | .MenuLink i { 26 | justify-self: end; 27 | transition: inherit; 28 | } 29 | 30 | a.MenuLink:hover, 31 | .Menu--active > .MenuLink, 32 | .Menu--childActive > .MenuLink { 33 | background-color: var(--color-main-02); 34 | } 35 | 36 | a.MenuLink:hover i, 37 | .Menu--active > .MenuLink i, 38 | .Menu--childActive > .MenuLink i { 39 | color: var(--color-main-01); 40 | } 41 | 42 | .MenuLink--big { 43 | font-size: var(--font-size--lg); 44 | grid-template-columns: 1.5rem auto; 45 | margin-left: 0; 46 | } 47 | 48 | .MenuLink--big i { 49 | font-size: 1.1em; 50 | } 51 | -------------------------------------------------------------------------------- /assets/styles/components/rating.css: -------------------------------------------------------------------------------- 1 | .Rating { 2 | color: rgba(var(--bs-secondary-rgb), 1); 3 | } 4 | -------------------------------------------------------------------------------- /assets/styles/components/sidebar.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 768px) { 2 | .sidebar .offcanvas-lg { 3 | position: sticky; 4 | top: 48px; 5 | } 6 | } 7 | 8 | @media (min-width: 1200px) { 9 | .modal-xl { 10 | --bs-modal-width: 90%; 11 | } 12 | } 13 | 14 | .sidebar .nav-link { 15 | font-size: var(--font-size--sm); 16 | font-weight: 500; 17 | } 18 | 19 | .sidebar .nav-item.active > .nav-link { 20 | font-weight: bold; 21 | color: brown; 22 | } 23 | .sidebar .icon-link .bi { 24 | height: auto; 25 | } 26 | 27 | .sidebar-heading { 28 | font-size: var(--font-size--xs); 29 | color: brown; 30 | } 31 | -------------------------------------------------------------------------------- /assets/styles/components/table.css: -------------------------------------------------------------------------------- 1 | .Table { 2 | width: 100%; 3 | } 4 | 5 | .Table > :not(caption) > * > * { 6 | padding: 0.5rem; 7 | } 8 | -------------------------------------------------------------------------------- /assets/styles/global.scss: -------------------------------------------------------------------------------- 1 | // the ~ allows you to reference things in node_modules 2 | @import "~bootstrap/scss/bootstrap"; 3 | @import "~bootstrap-icons/font/bootstrap-icons.css"; 4 | @import "./overrides.css"; 5 | 6 | @import "./variables.css"; 7 | 8 | @import "./layout.css"; 9 | 10 | @import "./components/assistant.css"; 11 | @import "./components/book.css"; 12 | @import "./components/bookDetails.css"; 13 | @import "./components/bookGrid.css"; 14 | @import "./components/bookReader.css"; 15 | @import "./components/bookWithDetails.css"; 16 | @import "./components/card.css"; 17 | @import "./components/facets.css"; 18 | @import "./components/heading.css"; 19 | @import "./components/hero.css"; 20 | @import "./components/heroProgress.css"; 21 | @import "./components/menuBlock.css"; 22 | @import "./components/rating.css"; 23 | @import "./components/sidebar.css"; 24 | @import "./components/table.css"; 25 | @import "./components/login.css"; 26 | -------------------------------------------------------------------------------- /assets/styles/overrides.css: -------------------------------------------------------------------------------- 1 | body.bg-darker { 2 | background-color: #000; 3 | color: #fff; 4 | } 5 | 6 | .bi { 7 | height: auto; 8 | } 9 | 10 | .navbar-brand { 11 | padding-top: 0.75rem; 12 | padding-bottom: 0.75rem; 13 | background-color: rgba(0, 0, 0, 0.25); 14 | box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25); 15 | } 16 | 17 | .navbar .form-control { 18 | padding: 0.75rem 1rem; 19 | } 20 | -------------------------------------------------------------------------------- /assets/styles/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --space--xs: 0.25rem; 3 | --space--sm: 0.5rem; 4 | --space--md: 1rem; 5 | --space--lg: 1.5rem; 6 | --space--xl: 2rem; 7 | 8 | --border-radius--sm: 0.25rem; 9 | --border-radius--md: 0.5rem; 10 | --border-radius--lg: 1rem; 11 | 12 | --font-size--xs: 0.75rem; 13 | --font-size--sm: 0.875rem; 14 | --font-size--md: 1rem; 15 | --font-size--lg: 1.25rem; 16 | --font-size--xl: 1.5rem; 17 | 18 | --body-background-color: #f2f2f2; 19 | 20 | --color-main-01: #de9779; /* cover background, icon color, progress bar progress */ 21 | --color-main-02: #eddcd5; /* nav background, progress bar background, block emphasis */ 22 | --color-main-03: #f1edeb; /* block background */ 23 | } 24 | 25 | [data-bs-theme=dark] { 26 | --body-background-color: #0d0d0d; 27 | --color-main-01: #bf4f20; 28 | --color-main-02: #4f2311; 29 | --color-main-03: #241e1b; 30 | } 31 | -------------------------------------------------------------------------------- /backups/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/backups/.gitkeep -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | -------------------------------------------------------------------------------- /doc/src/assets/natural_language.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/doc/src/assets/natural_language.gif -------------------------------------------------------------------------------- /doc/src/assets/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/doc/src/assets/summary.png -------------------------------------------------------------------------------- /doc/src/assets/tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/doc/src/assets/tags.png -------------------------------------------------------------------------------- /doc/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsLoader } from "@astrojs/starlight/loaders"; 3 | import { docsSchema } from '@astrojs/starlight/schema'; 4 | 5 | export const collections = { 6 | docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 7 | }; 8 | -------------------------------------------------------------------------------- /doc/src/content/docs/Troubleshooting/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Updating this documentation 3 | --- 4 | 5 | This documentation is hosted on [GitHub](https://github.com/biblioverse/biblioteca-doc) and is open source. If you find an error or would like to contribute, please feel 6 | free to make a pull request or open an issue on the project. -------------------------------------------------------------------------------- /doc/src/content/docs/Troubleshooting/github.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ask on github 3 | --- 4 | Please feel free to open issues or start discussions on [GitHub](https://github.com/biblioverse/biblioteca) if you have 5 | any questions or need help. We are happy to help you. 6 | 7 | New features and merge requests are also welcome! -------------------------------------------------------------------------------- /doc/src/content/docs/demo.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Demo 3 | description: Try it yourself 4 | --- 5 | import { LinkButton } from '@astrojs/starlight/components'; 6 | import { Aside } from '@astrojs/starlight/components'; 7 | 8 | 11 | 12 | You can try Biblioteca right now without installing it. 13 | 14 | You can login with the following accounts: 15 | 16 | - **Admin**: 17 | - Username: `admin` 18 | - **User**: 19 | - Username: `user` 20 | 21 | The password is in both cases the same value as the username. 22 | 23 | Try it now 24 | 25 | -------------------------------------------------------------------------------- /doc/src/content/docs/guides/Administrator/adding-users.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding users 3 | --- 4 | import { Aside } from '@astrojs/starlight/components'; 5 | 6 | ## First user 7 | You should have created your first admin user after installing the app. The first user is always an admin and you will need 8 | admin rights to add other users 9 | 10 | ## Adding new users 11 | If you are logged in with an administrator account, you will have a link to the user management page `/user`. 12 | 13 | Here you can add new users, change their roles, and set or change their passwords. 14 | 15 | You can select the maximum age category of books that the user can read. This is useful if you want to restrict access to children 16 | 17 | 20 | 21 | ## Roles 22 | User can have one of the following roles: 23 | - `ROLE_USER`: The default role for new users. They can read books, change their own settings, create their own shelves and sync their Kobo devices. 24 | - `ROLE_ADMIN`: Can do everything a user can do and manage users and books. -------------------------------------------------------------------------------- /doc/src/content/docs/guides/Administrator/deleting-books.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deleting books 3 | --- 4 | import { Aside } from '@astrojs/starlight/components'; 5 | 6 | 11 | 12 | On every book page, on the bottom of the page, there is a button to delete the book. 13 | This will remove the book from the database **and the filesystem**. -------------------------------------------------------------------------------- /doc/src/content/docs/guides/Administrator/edit-book.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Edit book metadata 3 | --- 4 | 5 | There are different ways to edit the metadata of a book in Biblioteca. 6 | 7 | ## On the book page 8 | 9 | On the book page, for each editable field you can click on the pencil icon to make the field editable. 10 | 11 | After you have made the changes, you can click on the checkmark icon to save the changes. 12 | 13 | ## On the "All books" page 14 | On the top right of the list of books, you can change the mode to list mode instead of the gallery view. 15 | In list mode, you can edit the title, author and serie of a book. 16 | 17 | ## Multiple book edition at once 18 | On the top right of the list of books, you can change the mode to list mode instead of the gallery view. 19 | On top of the list of books, you can find a button to edit all displayed books at once for serie, author and tags. -------------------------------------------------------------------------------- /doc/src/content/docs/guides/Administrator/filesystem.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Filesystem and relocation 3 | --- 4 | 5 | You can manage the file structure of your book by hand and run a command to have them in biblioteca, or you can ask biblioteca 6 | to relocate them to a path format of your choice. -------------------------------------------------------------------------------- /doc/src/content/docs/guides/Administrator/instance-configuration.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Instance Configuration 3 | --- 4 | 5 | Go to `/configuration` to configure or see the .env.local variables that can be configured. -------------------------------------------------------------------------------- /doc/src/content/docs/guides/Administrator/verified.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Verified books 3 | --- 4 | 5 | The verification status is a flag on the books. When you add a book, it will be set to false. 6 | 7 | If you are an administrator, there is a link in the application menu listing all boks that have not been verified yet. 8 | 9 | This feature allows you to know which books have been added recently and for whom you need to verify and update the metadata. 10 | 11 | When the flag is checked, the edition controls are removed until you uncheck it. -------------------------------------------------------------------------------- /doc/src/content/docs/guides/User/OPDS.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: OPDS Catalog 3 | --- 4 | import { Steps } from '@astrojs/starlight/components'; 5 | 6 | 7 | It is possible to sync your Kobo devices with Biblioteca. 8 | 9 | 10 | 1. Go to the "My Profile" page on the OPDS tab and click on the "Create token" button. 11 | 2. An URL for your opds feed will be generated. 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /doc/src/content/docs/guides/User/homepage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Your homepage 3 | --- 4 | 5 | On your homepage, you will see the list of books that you have started reading and their progress. 6 | 7 | You can also see the books that you have marked as "In my reading List" and below, some suggestions based on tags. 8 | 9 | All the books series that you have started reading will also appear on your homepage, so you can see where you are in your 10 | progress. Completionists beware! 11 | 12 | When you read books in the app or on your kobo, the progress will be updated in the app. 13 | 14 | -------------------------------------------------------------------------------- /doc/src/content/docs/guides/User/interactions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Book interaction 3 | --- 4 | 5 | You can mark any book as "Read", "Don't want to read" or "In my reading List" by using the buttons below the book cover 6 | on the book's page. 7 | 8 | Books that you have marked as "Read" will appear in your reading timeline and bey greyed out in the lists. 9 | You can access your reading timeline by clicking on the "Reading timeline" link in the navigation bar. 10 | 11 | Books that you have marked as "Don't want to read" will be hidden from the lists and will not appear in your reading timeline. 12 | 13 | Books that you have marked as "In my reading List" will appear on your homepage. -------------------------------------------------------------------------------- /doc/src/content/docs/guides/User/searching.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Searching for books 3 | --- 4 | 5 | There are several ways to search for books in the library. 6 | 7 | ## Quick search 8 | On top of every page, you can find a search bar. You can search for books by title, author, series, or tags. 9 | The search is case-insensitive and will return all books that match the search query. 10 | 11 | This is mostly useful when you know what you are looking for and want to find it quickly. 12 | 13 | 14 | ## Filtering 15 | In the search bar, you can click on advanced filters, which will allow you to write a typesense filter query. 16 | The documentation for filtering typesense queries can be found here: https://typesense.org/docs/27.1/api/search.html#filter-parameters 17 | 18 | If AI features are enabled, you can also write a natural language query and ask AI to convert it to filters by clicking the magic wand button: 19 | 20 | ![Search for books](../../../../assets/natural_language.gif) -------------------------------------------------------------------------------- /doc/src/content/docs/guides/User/shelf.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Shelves 3 | --- 4 | 5 | Shelves are groups of books, they can be static or dynamic. 6 | 7 | You can manually add books to static shelves and that's it. 8 | 9 | You can also create dynamic shelves, which are based on a query. For example, you can create a shelf that contains all 10 | books from your favorite authors that you haven't read and if you or an administrator adds books from these authors, 11 | they will automatically be listed in there. 12 | 13 | ## Create a dynamic Shelf 14 | To do this, head to the "All books" page, and update the filters in the top `Filters` sections. When you are satisfied 15 | with the result, click `Save current filter`, input the name of your shelf and you're good to go! 16 | 17 | ## Create a static shelf 18 | In your side menu click on "Edit Shelves". Here you can edit or delete all your existing shelves. At the bottom of the page, 19 | you can create a new Shelf. 20 | Once you have created a shelf, head on to the books you want to add in that shelves and below the cover, the list of the 21 | shelves will be displayed. Add them by clicking on the shelf's name and remove them by clicking again. 22 | -------------------------------------------------------------------------------- /doc/src/content/docs/guides/User/sync-kobo.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sync Kobo Devices 3 | --- 4 | import { Steps } from '@astrojs/starlight/components'; 5 | 6 | 7 | It is possible to sync your Kobo devices with Biblioteca. 8 | 9 | 10 | 1. Create a new [static shelf](/guides/user/shelf) on your profile. And add some books in it 11 | 2. Go to the "My Profile" page on the Kobo Settings tab and click on the "Create new Kobo device" button. 12 | 3. Follow the instructions on the kobo device page in biblioteca. 13 | 14 | You will need to plug your kobo device to your computer and edit a configuration file. 15 | The lines to edit and the values to enter are detailed in the app. 16 | 17 | 4. Once the configuration file is edited, unmount your kobo device and click on the "Sync" button on the kobo device. 18 | 5. Read your books on the Kobo device and sync it again to update your progress on Biblioteca. 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /doc/src/content/docs/guides/User/update-settings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Updating your settings 3 | --- 4 | 5 | On the sidebar, you will see a panel with your username. Here you can click on the "My Profile link" to edit your profile. 6 | 7 | On this page, you can change your password, the display language and enable or disable some menu categories based on your 8 | preferences. 9 | 10 | You can also change your theme. 11 | 12 | If you are an administrator, you can also configure your [AI settings](../../administrator/ai-features-configuration) -------------------------------------------------------------------------------- /doc/src/content/docs/installing/2-new-versions.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Updating to new versions 3 | --- 4 | import { Steps } from '@astrojs/starlight/components'; 5 | import { Aside } from '@astrojs/starlight/components'; 6 | 7 | 10 | 11 | 12 | 1. Update the docker image to the latest version: 13 | 14 | ```bash 15 | docker-compose pull 16 | docker-compose up -d 17 | ``` 18 | 19 | 2. Run the following command to update the database schema: 20 | 21 | ```bash 22 | docker compose exec biblioteca bin/console doctrine:migration:migrate --no-interaction 23 | ``` 24 | 25 | 3. If needed, update the typesense schema: 26 | 27 | ```bash 28 | docker-compose exec biblioteca bin/console biblioverse:typesense:populate 29 | ``` 30 | 31 | 4. Clear the cache: 32 | 33 | ```bash 34 | docker-compose exec biblioteca bin/console cache:clear 35 | ``` 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /doc/src/content/docs/installing/unraid.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Unraid 3 | --- 4 | You can use and install Biblioteca on your Unraid system by using a docker-compose manager, like 5 | [Dockge](https://github.com/louislam/dockge) -------------------------------------------------------------------------------- /doc/src/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --sl-content-width: 90%; 3 | --sl-text-5xl: 3.5rem; 4 | } -------------------------------------------------------------------------------- /doc/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /doc/src/files/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | biblioteca: 3 | image: ghcr.io/biblioverse/biblioteca:main 4 | command: ["/bin/sh", "-c" , "apache2-foreground" ] 5 | ports: 6 | - 8080 7 | depends_on: 8 | - db 9 | stdin_open: true 10 | tty: true 11 | volumes: 12 | - :/var/www/html/public/covers 13 | - :/var/www/html/public/books 14 | - :/var/www/html/public/media 15 | - .env:/var/www/html/.env 16 | db: 17 | image: mariadb:11.7 18 | environment: 19 | - MYSQL_ROOT_PASSWORD=db 20 | - MYSQL_DATABASE=db 21 | - MYSQL_USER=db 22 | - MYSQL_PASSWORD=db 23 | volumes: 24 | - mariadb:/var/lib/mysql 25 | 26 | typesense: 27 | image: typesense/typesense:28.0 28 | restart: on-failure 29 | ports: 30 | - 8983 31 | - 8108 32 | volumes: 33 | - searchdata:/data 34 | command: '--data-dir /data --api-key=xyz --enable-cors' 35 | 36 | volumes: 37 | mariadb: 38 | searchdata: -------------------------------------------------------------------------------- /doc/src/files/helm-values.yml: -------------------------------------------------------------------------------- 1 | biblioteca: 2 | appSecret: 3 | appSecret: zafmqUbgaMQbx4wCFbZSpwsQ34Dw7wUd 4 | 5 | persistence: 6 | enabled: true 7 | -------------------------------------------------------------------------------- /doc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } 4 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | services: 2 | biblioteca: 3 | build: 4 | target: dev -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set shell := ["docker", "compose", "run", "--entrypoint", "/bin/sh", "-it", "--rm", "biblioteca", "-c"] 2 | composer *args="": 3 | /usr/local/bin/composer {{args}} 4 | 5 | sh *args="": 6 | sh {{args}} 7 | 8 | php *args="": 9 | php {{args}} 10 | 11 | console *args="": 12 | php bin/console {{args}} 13 | 14 | console-xdebug *args="": 15 | env PHP_IDE_CONFIG="serverName=biblioteca.docker.test" XDEBUG_TRIGGER=1 XDEBUG_MODE=debug php bin/console {{args}} 16 | 17 | install: 18 | composer install 19 | 20 | tests: 21 | composer run test 22 | 23 | update *args="": 24 | composer update {{args}} 25 | 26 | lint: 27 | composer run lint 28 | 29 | rector: 30 | composer rector 31 | 32 | test-phpcs: 33 | composer run test-phpcs 34 | 35 | phpcs: 36 | composer run phpcs 37 | 38 | phpunit *args="": 39 | env XDEBUG_MODE=coverage composer run test-phpunit -- {{args}} 40 | 41 | phpunit-xdebug *args="": 42 | composer test-phpunit-xdebug -- {{args}} 43 | 44 | phpstan *args="": 45 | composer test-phpstan -- {{args}} 46 | 47 | npm *args="": 48 | npm -- {{args}} 49 | -------------------------------------------------------------------------------- /migrations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/migrations/.gitignore -------------------------------------------------------------------------------- /migrations/AbstractMigration.php: -------------------------------------------------------------------------------- 1 | getTable($table); 15 | 16 | return true; 17 | } catch (TableDoesNotExist|SchemaException) { 18 | return false; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/Version20240121162659.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE book ADD age_category INT DEFAULT NULL'); 24 | $this->addSql('ALTER TABLE user ADD birthday DATE DEFAULT NULL, ADD max_age_category INT DEFAULT NULL'); 25 | } 26 | 27 | public function down(Schema $schema): void 28 | { 29 | // this down() migration is auto-generated, please modify it to your needs 30 | $this->addSql('ALTER TABLE book DROP age_category'); 31 | $this->addSql('ALTER TABLE `user` DROP birthday, DROP max_age_category'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrations/Version20240505101909.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE user ADD open_aikey VARCHAR(255) DEFAULT NULL, ADD book_summary_prompt LONGTEXT DEFAULT NULL, ADD book_keyword_prompt LONGTEXT DEFAULT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE `user` DROP open_aikey, DROP book_summary_prompt, DROP book_keyword_prompt'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20240510173645.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE user ADD theme VARCHAR(255) DEFAULT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE `user` DROP theme'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20240512162606.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE book ADD page_number INT DEFAULT NULL'); 24 | $this->addSql('ALTER TABLE book_interaction ADD read_pages INT NOT NULL'); 25 | } 26 | 27 | public function down(Schema $schema): void 28 | { 29 | // this down() migration is auto-generated, please modify it to your needs 30 | $this->addSql('ALTER TABLE book_interaction DROP read_pages'); 31 | $this->addSql('ALTER TABLE book DROP page_number'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrations/Version20240513152414.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE book_interaction CHANGE read_pages read_pages INT DEFAULT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE book_interaction CHANGE read_pages read_pages INT NOT NULL'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20240714144826.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE book_interaction ADD hidden TINYINT(1) NOT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE book_interaction DROP hidden'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20240721080644.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE user ADD last_login DATETIME DEFAULT NULL, ADD language VARCHAR(2) NULL DEFAULT \'en\', ADD use_kobo_devices TINYINT(1) NULL DEFAULT 1'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE `user` DROP last_login, DROP language, DROP use_kobo_devices'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20241121135658.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE kobo_device ADD device_id VARCHAR(255) DEFAULT NULL'); 21 | $this->addSql('CREATE INDEX kobo_device_id ON kobo_device (device_id);'); 22 | } 23 | 24 | public function down(Schema $schema): void 25 | { 26 | $this->addSql('DROP INDEX kobo_device_id ON kobo_device;'); 27 | $this->addSql('ALTER TABLE kobo_device DROP device_id;'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /migrations/Version20241121142239.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE kobo_device ADD model VARCHAR(255) DEFAULT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE kobo_device DROP model'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20241129185216.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE kobo_device ADD upstream_sync TINYINT(1) DEFAULT 0 NOT NULL'); 23 | } 24 | 25 | public function down(Schema $schema): void 26 | { 27 | $this->addSql('ALTER TABLE kobo_device DROP upstream_sync'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /migrations/Version20241208200901.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE opds_access (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, token VARCHAR(255) NOT NULL, INDEX IDX_1F8403F8A76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB'); 24 | $this->addSql('ALTER TABLE opds_access ADD CONSTRAINT FK_1F8403F8A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)'); 25 | } 26 | 27 | public function down(Schema $schema): void 28 | { 29 | // this down() migration is auto-generated, please modify it to your needs 30 | $this->addSql('ALTER TABLE opds_access DROP FOREIGN KEY FK_1F8403F8A76ED395'); 31 | $this->addSql('DROP TABLE opds_access'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrations/Version20241210143123.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE kobo_device ADD sync_reading_list TINYINT(1) DEFAULT 1 NOT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE kobo_device DROP sync_reading_list'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20241218154713.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE book_interaction ADD rating INT DEFAULT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE book_interaction DROP rating'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20241223182304.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE shelf CHANGE query_string query_string LONGTEXT DEFAULT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE shelf CHANGE query_string query_string JSON DEFAULT NULL COMMENT \'(DC2Type:json)\''); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20250101174217.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE shelf ADD query_filter LONGTEXT DEFAULT NULL, ADD query_order VARCHAR(255) DEFAULT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE shelf DROP query_filter, DROP query_order'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20250106061005.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE instance_configuration (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, value LONGTEXT DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('DROP TABLE instance_configuration'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20250106090320.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE ai_model (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) NOT NULL, model VARCHAR(255) NOT NULL, token LONGTEXT DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('DROP TABLE ai_model'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20250106091638.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE ai_model ADD url VARCHAR(255) NOT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | $this->addSql('ALTER TABLE ai_model DROP url'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /migrations/Version20250106094820.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE ai_model ADD system_prompt LONGTEXT DEFAULT NULL, ADD use_epub_context TINYINT(1) NOT NULL, ADD use_wikipedia_context TINYINT(1) NOT NULL, ADD use_amazon_context TINYINT(1) NOT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE ai_model DROP system_prompt, DROP use_epub_context, DROP use_wikipedia_context, DROP use_amazon_context'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20250128213230.php: -------------------------------------------------------------------------------- 1 | addSql(' 23 | DELETE FROM kobo_synced_book 24 | WHERE id NOT IN ( 25 | SELECT id FROM ( 26 | SELECT MAX(id) AS id 27 | FROM kobo_synced_book 28 | GROUP BY book_id, kobo_device_id 29 | ) AS subquery 30 | ) 31 | '); 32 | } 33 | 34 | public function down(Schema $schema): void 35 | { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /migrations/Version20250128214238.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE UNIQUE INDEX kobo_synced_book_unique ON kobo_synced_book (book_id, kobo_device_id)'); 23 | } 24 | 25 | public function down(Schema $schema): void 26 | { 27 | $this->addSql('DROP INDEX kobo_synced_book_unique ON kobo_synced_book'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /migrations/Version20250130192538.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE kobo_synced_book ADD archived DATETIME DEFAULT NULL'); 23 | } 24 | 25 | public function down(Schema $schema): void 26 | { 27 | $this->addSql('ALTER TABLE kobo_synced_book DROP archived'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /migrations/Version20250131184632.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE kobo_device ADD last_sync_token LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json)\''); 24 | $this->addSql('ALTER TABLE kobo_device CHANGE last_sync_token last_sync_token JSON DEFAULT NULL'); 25 | } 26 | 27 | public function down(Schema $schema): void 28 | { 29 | // this down() migration is auto-generated, please modify it to your needs 30 | $this->addSql('ALTER TABLE kobo_device DROP last_sync_token'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /migrations/Version20250202073734.php: -------------------------------------------------------------------------------- 1 | addSql('DELETE FROM book_interaction WHERE book_id is null or user_id is null'); 24 | $this->addSql('ALTER TABLE book_interaction CHANGE user_id user_id INT NOT NULL, CHANGE book_id book_id INT NOT NULL'); 25 | } 26 | 27 | public function down(Schema $schema): void 28 | { 29 | // this down() migration is auto-generated, please modify it to your needs 30 | $this->addSql('ALTER TABLE book_interaction CHANGE user_id user_id INT DEFAULT NULL, CHANGE book_id book_id INT DEFAULT NULL'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /migrations/Version20250202150749.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE UNIQUE INDEX unique_user_book ON book_interaction (user_id, book_id)'); 20 | } 21 | 22 | public function down(Schema $schema): void 23 | { 24 | $this->addSql('DROP INDEX unique_user_book ON book_interaction'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /migrations/Version20250213182540.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE ai_model DROP use_epub_context, DROP use_wikipedia_context, DROP use_amazon_context'); 24 | $this->addSql('ALTER TABLE user DROP book_summary_prompt, DROP book_keyword_prompt'); 25 | } 26 | 27 | public function down(Schema $schema): void 28 | { 29 | // this down() migration is auto-generated, please modify it to your needs 30 | $this->addSql('ALTER TABLE ai_model ADD use_epub_context TINYINT(1) DEFAULT 0 NOT NULL, ADD use_wikipedia_context TINYINT(1) DEFAULT 0 NOT NULL, ADD use_amazon_context TINYINT(1) DEFAULT 0 NOT NULL'); 31 | $this->addSql('ALTER TABLE `user` ADD book_summary_prompt LONGTEXT DEFAULT NULL, ADD book_keyword_prompt LONGTEXT DEFAULT NULL'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | tipsOfTheDay: false 3 | level: 9 4 | paths: 5 | - src/ 6 | - tests/ 7 | ignoreErrors: 8 | - identifier: missingType.generics 9 | - '#(.*)no value type specified in iterable type array#' 10 | # - '#Asserted type (.*) for (.*) with type (.*) does not narrow down the type.#' 11 | errorFormat: symplify 12 | typeAliases: 13 | ReadingStateCriteria: "array{'book':int, 'readPages': int|null, 'readStatus': App\\Enum\\ReadStatus}" 14 | -------------------------------------------------------------------------------- /public/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/public/images/.gitkeep -------------------------------------------------------------------------------- /public/images/blank.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/public/images/blank.jpg -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); 8 | -------------------------------------------------------------------------------- /public/media/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/public/media/.gitkeep -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 14 | __DIR__ . '/config', 15 | __DIR__ . '/public', 16 | __DIR__ . '/src', 17 | __DIR__ . '/tests', 18 | ]) 19 | ->withRules([ 20 | AddVoidReturnTypeWhereNoReturnRector::class, 21 | ]) 22 | ->withPreparedSets( 23 | deadCode: true, 24 | codeQuality: true 25 | ) 26 | ->withSets([ 27 | LevelSetList::UP_TO_PHP_83, 28 | SymfonySetList::SYMFONY_64, 29 | SymfonySetList::SYMFONY_72, 30 | SymfonySetList::SYMFONY_CODE_QUALITY, 31 | SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, 32 | ]) 33 | ->withImportNames(true, true, false, true) 34 | ->withSkip([ 35 | '**/config/bundles.php', 36 | InlineClassRoutePrefixRector::class 37 | ]); 38 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | "schedule:weekends", 6 | ":dependencyDashboard", 7 | ":automergeMinor", 8 | "group:allNonMajor" 9 | ], 10 | "major": { 11 | "dependencyDashboardApproval": true 12 | }, 13 | "lockFileMaintenance": { 14 | "enabled": true 15 | }, 16 | "packageRules": [ 17 | { 18 | "matchManagers": ["composer"], 19 | "rangeStrategy": "update-lockfile" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/Ai/Communicator/AbstractCommunicator.php: -------------------------------------------------------------------------------- 1 | aiModel = $model; 15 | } 16 | 17 | #[\Override] 18 | public function getAiModel(): AiModel 19 | { 20 | return $this->aiModel; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Ai/Communicator/AiAction.php: -------------------------------------------------------------------------------- 1 | 20])] 9 | interface AiChatInterface extends AiCommunicatorInterface 10 | { 11 | /** 12 | * @param Message[] $messages 13 | */ 14 | public function chat(array $messages): string; 15 | } 16 | -------------------------------------------------------------------------------- /src/Ai/Communicator/AiCommunicatorInterface.php: -------------------------------------------------------------------------------- 1 | 20])] 9 | interface AiCommunicatorInterface 10 | { 11 | public function initialise(AiModel $model): void; 12 | 13 | public function getAiModel(): AiModel; 14 | 15 | public function interrogate(string $prompt): string; 16 | } 17 | -------------------------------------------------------------------------------- /src/Ai/Context/ContextBuildingInterface.php: -------------------------------------------------------------------------------- 1 | 20])] 11 | interface ContextBuildingInterface 12 | { 13 | public function isEnabled(AiModel $aiModel, ?Book $book = null): bool; 14 | 15 | public function getContextForPrompt(BookPromptInterface $prompt): string; 16 | } 17 | -------------------------------------------------------------------------------- /src/Ai/Message.php: -------------------------------------------------------------------------------- 1 | date = new \DateTimeImmutable(); 15 | } 16 | 17 | public function toOpenAI(): array 18 | { 19 | return [ 20 | 'role' => $this->role->value, 21 | 'content' => $this->text, 22 | ]; 23 | } 24 | 25 | public function getText(): string 26 | { 27 | return $this->text; 28 | } 29 | 30 | public function toPerplexica(): array 31 | { 32 | $roleValue = match ($this->role) { 33 | AiMessageRole::System => 'human', 34 | AiMessageRole::User => 'human', 35 | AiMessageRole::Assistant => 'assistant', 36 | }; 37 | 38 | return [$roleValue, $this->text]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Ai/Prompt/AbstractBookPrompt.php: -------------------------------------------------------------------------------- 1 | prompt; 23 | } 24 | 25 | #[\Override] 26 | public function setPrompt(string $prompt): void 27 | { 28 | $this->prompt = $prompt; 29 | } 30 | 31 | #[\Override] 32 | public function getBook(): Book 33 | { 34 | return $this->book; 35 | } 36 | 37 | #[\Override] 38 | public function replaceBookOccurrence(string $prompt): string 39 | { 40 | $bookString = $this->book->getPromptString(); 41 | 42 | $language = $this->book->getLanguage() ?? $this->language; 43 | 44 | return str_replace(['{book}', '{language}'], [$bookString, $this->getFullLanguageName($language)], $prompt); 45 | } 46 | 47 | private function getFullLanguageName(string $language): string 48 | { 49 | if (strlen($language) !== 2 || !class_exists('\Locale')) { 50 | return $language; 51 | } 52 | 53 | return \Locale::getDisplayLanguage($language); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Ai/Prompt/BookPromptInterface.php: -------------------------------------------------------------------------------- 1 | security->getUser(); 21 | if ($language === null) { 22 | $language = $book->getLanguage(); 23 | } 24 | if ($language === null && $user instanceof User) { 25 | $language = $user->getLanguage(); 26 | } 27 | if ($language === null) { 28 | $language = 'en'; 29 | } 30 | 31 | /** @var BookPromptInterface $object */ 32 | $object = new $class($book, $this->config, $language); 33 | $object->initialisePrompt(); 34 | 35 | return $object; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Controller/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/src/Controller/.gitignore -------------------------------------------------------------------------------- /src/Controller/Kobo/AbstractKoboController.php: -------------------------------------------------------------------------------- 1 | Hello Kobo'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Controller/Kobo/Api/V1/DownloadController.php: -------------------------------------------------------------------------------- 1 | '\d+', 'extension' => '[A-Za-z0-9]+'], methods: ['GET'])] 23 | public function download(Book $book, string $extension): Response 24 | { 25 | $this->denyAccessUnlessGranted(BookVoter::DOWNLOAD, $book, 'You are not allowed to download this book'); 26 | 27 | return $this->downloadHelper->getResponse($book, $extension); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Controller/Kobo/Api/V1/Library/MetadataController.php: -------------------------------------------------------------------------------- 1 | koboStoreProxy->isEnabled()) { 29 | return $this->koboStoreProxy->proxy($request); 30 | } 31 | 32 | return new JsonResponse(['error' => 'Book not found'], Response::HTTP_NOT_FOUND); 33 | } 34 | 35 | return $this->syncResponseFactory->createMetadata($koboDevice, $book); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Controller/Kobo/Api/V1/ProductsController.php: -------------------------------------------------------------------------------- 1 | '^[a-zA-Z0-9\-]+$'], methods: ['GET', 'POST'])] 20 | public function nextRead(Request $request): Response 21 | { 22 | if ($this->koboStoreProxy->isEnabled()) { 23 | return $this->koboStoreProxy->proxyOrRedirect($request); 24 | } 25 | 26 | return new JsonResponse([]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Controller/LoginController.php: -------------------------------------------------------------------------------- 1 | getLastAuthenticationError(); 16 | 17 | // last username entered by the user 18 | $lastUsername = $authenticationUtils->getLastUsername(); 19 | 20 | return $this->render('login/login.html.twig', [ 21 | // parameters usually defined in Symfony login forms 22 | 'error' => $error, 23 | 'last_username' => $lastUsername, 24 | ]); 25 | } 26 | 27 | #[Route('/logout', name: 'app_logout', methods: ['GET'])] 28 | public function logout(): void 29 | { 30 | // controller can be blank: it will never be called! 31 | throw new \Exception('Don\'t forget to activate logout in security.yaml'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Controller/ShelfController.php: -------------------------------------------------------------------------------- 1 | getBooksInShelf($shelf); 16 | 17 | return $this->render('shelf/index.html.twig', [ 18 | 'shelf' => $shelf, 19 | 'books' => $books, 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DataFixtures/KoboFixture.php: -------------------------------------------------------------------------------- 1 | setAccessKey(self::ACCESS_KEY); 24 | $kobo->setName('test kobo'); 25 | $kobo->setUser($this->getUser(UserFixture::CHILD_USER_REFERENCE)); 26 | 27 | $manager->persist($kobo); 28 | $manager->flush(); 29 | $this->addReference(self::KOBO_REFERENCE, $kobo); 30 | } 31 | 32 | protected function getUser(string $reference = UserFixture::USER_REFERENCE): User 33 | { 34 | return $this->getReference($reference, User::class); 35 | } 36 | 37 | #[\Override] 38 | public function getDependencies(): array 39 | { 40 | return [ 41 | UserFixture::class, 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DataFixtures/OpdsAccessFixture.php: -------------------------------------------------------------------------------- 1 | getUser()); 22 | $opdsAccess->setToken(self::ACCESS_KEY); 23 | 24 | $manager->persist($opdsAccess); 25 | $manager->flush(); 26 | } 27 | 28 | protected function getUser(): User 29 | { 30 | return $this->getReference(UserFixture::USER_REFERENCE, User::class); 31 | } 32 | 33 | #[\Override] 34 | public function getDependencies(): array 35 | { 36 | return [ 37 | UserFixture::class, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/DataFixtures/ShelfKoboFixture.php: -------------------------------------------------------------------------------- 1 | getReference(KoboFixture::KOBO_REFERENCE, KoboDevice::class); 20 | 21 | $kobo->addShelf($this->getReference(ShelfFixture::SHELF_REFERENCE, Shelf::class)); 22 | 23 | $manager->flush(); 24 | } 25 | 26 | #[\Override] 27 | public function getDependencies(): array 28 | { 29 | return [ 30 | ShelfFixture::class, 31 | KoboFixture::class, 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Entity/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/src/Entity/.gitignore -------------------------------------------------------------------------------- /src/Entity/InstanceConfiguration.php: -------------------------------------------------------------------------------- 1 | id; 28 | } 29 | 30 | public function getName(): ?string 31 | { 32 | return $this->name; 33 | } 34 | 35 | public function setName(string $name): static 36 | { 37 | $this->name = $name; 38 | 39 | return $this; 40 | } 41 | 42 | public function getValue(): ?string 43 | { 44 | return $this->value; 45 | } 46 | 47 | public function setValue(?string $value): static 48 | { 49 | $this->value = $value; 50 | 51 | return $this; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Entity/OpdsAccess.php: -------------------------------------------------------------------------------- 1 | id; 28 | } 29 | 30 | public function getToken(): ?string 31 | { 32 | return $this->token; 33 | } 34 | 35 | public function setToken(?string $token): static 36 | { 37 | $this->token = $token; 38 | 39 | return $this; 40 | } 41 | 42 | public function getUser(): ?User 43 | { 44 | return $this->user; 45 | } 46 | 47 | public function setUser(?User $user): static 48 | { 49 | $this->user = $user; 50 | 51 | return $this; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Entity/RandomGeneratorTrait.php: -------------------------------------------------------------------------------- 1 | 16 bytes 17 | 18 | // Convert to a hexadecimal string 19 | return substr(bin2hex($bytes), 0, $length); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Entity/UuidGeneratorTrait.php: -------------------------------------------------------------------------------- 1 | 'primary', 15 | self::System => 'dark', 16 | self::Assistant => 'light', 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Enum/ReadStatus.php: -------------------------------------------------------------------------------- 1 | 'enum.readstatus.not-started', 20 | self::Started => 'enum.readstatus.started', 21 | self::Finished => 'enum.readstatus.finished', 22 | }; 23 | } 24 | 25 | public static function getIcon(self $value): string 26 | { 27 | return match ($value) { 28 | self::NotStarted => 'question-circle', 29 | self::Started => 'hourglass-split', 30 | self::Finished => 'check-circle-fill', 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Enum/ReadingList.php: -------------------------------------------------------------------------------- 1 | 'enum.readinglist.toread', 20 | self::Ignored => 'enum.readinglist.ignored', 21 | self::NotDefined => 'enum.readinglist.notdefined', 22 | }; 23 | } 24 | 25 | public static function getIcon(self $value): string 26 | { 27 | return match ($value) { 28 | self::ToRead => 'bookmark-heart-fill', 29 | self::Ignored => 'bookmark-x-fill', 30 | self::NotDefined => 'bookmark', 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/EventListener/LanguageListener.php: -------------------------------------------------------------------------------- 1 | getRequest(); 22 | 23 | if (!$request->hasSession()) { 24 | return; 25 | } 26 | $config = $this->security->getFirewallConfig($request); 27 | if ($config instanceof FirewallConfig && $config->isStateless()) { 28 | return; 29 | } 30 | 31 | $locale = $this->requestStack->getSession()->get('_locale'); 32 | if (!is_string($locale)) { 33 | return; 34 | } 35 | 36 | $request->setLocale($locale); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Exception/BookExtractionException.php: -------------------------------------------------------------------------------- 1 | add('name', null, [ 17 | 'disabled' => true, 18 | ]) 19 | ->add('value') 20 | ; 21 | } 22 | 23 | #[\Override] 24 | public function configureOptions(OptionsResolver $resolver): void 25 | { 26 | $resolver->setDefaults([ 27 | 'data_class' => InstanceConfiguration::class, 28 | 'label_translation_prefix' => 'instance-configuration.form.', 29 | ]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Form/KoboLastSyncTokenType.php: -------------------------------------------------------------------------------- 1 | setDefaults([ 14 | 'disabled' => true, 15 | 'attr' => [ 16 | 'readonly' => true, 17 | 'disabled' => true, 18 | ], 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Form/ShelfType.php: -------------------------------------------------------------------------------- 1 | add('name') 17 | ; 18 | 19 | if ($options['data'] instanceof Shelf && $options['data']->getQueryString() !== null) { 20 | $builder->add('queryString'); 21 | $builder->add('queryFilter'); 22 | $builder->add('queryOrder'); 23 | } 24 | } 25 | 26 | #[\Override] 27 | public function configureOptions(OptionsResolver $resolver): void 28 | { 29 | $resolver->setDefaults([ 30 | 'data_class' => Shelf::class, 31 | 'label_translation_prefix' => 'shelf.form.', 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | size; 14 | } 15 | 16 | public function getUrl(): string 17 | { 18 | return $this->url; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Kobo/Kepubify/KebpubifyCachedData.php: -------------------------------------------------------------------------------- 1 | size = (int) filesize($filename); 13 | $this->content = (string) file_get_contents($filename); 14 | } 15 | 16 | public function getSize(): int 17 | { 18 | return $this->size; 19 | } 20 | 21 | public function getContent(): string 22 | { 23 | return $this->content; 24 | } 25 | 26 | #[\Override] 27 | public function jsonSerialize(): array 28 | { 29 | return [ 30 | 'size' => $this->size, 31 | 'content' => $this->content, 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Kobo/Kepubify/KepubifyConversionFailed.php: -------------------------------------------------------------------------------- 1 | kepubifyBinary = $kepubifyBinary; 18 | } 19 | 20 | public function isEnabled(): bool 21 | { 22 | return trim($this->kepubifyBinary) !== ''; 23 | } 24 | 25 | public function getKepubifyBinary(): string 26 | { 27 | return $this->kepubifyBinary; 28 | } 29 | 30 | public function disable(): string 31 | { 32 | $lastValue = $this->kepubifyBinary; 33 | $this->kepubifyBinary = ''; 34 | 35 | return $lastValue; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Kobo/Kepubify/KepubifyMessage.php: -------------------------------------------------------------------------------- 1 | requestStack->getCurrentRequest(); 22 | 23 | if (!$request instanceof Request) { 24 | return $record; 25 | } 26 | 27 | if (false === $request->attributes->has('isKoboRequest')) { 28 | return $record; 29 | } 30 | 31 | $kobo = $this->getKoboFromRequest($request); 32 | $koboString = $kobo?->getId() ?? 'unknown'; 33 | $record->extra['kobo'] = $koboString; 34 | 35 | return $record; 36 | } 37 | 38 | private function getKoboFromRequest(Request $request): ?KoboDevice 39 | { 40 | $device = $request->attributes->get('koboDevice'); 41 | if ($device instanceof KoboDevice) { 42 | return $device; 43 | } 44 | 45 | return $this->koboParamConverter->apply($request); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Kobo/Proxy/KoboProxyLoggerFactory.php: -------------------------------------------------------------------------------- 1 | configuration, $this->koboProxyLogger, $accessToken); 20 | } 21 | 22 | public function createStack(string $accessToken): HandlerStack 23 | { 24 | $stack = new HandlerStack(); 25 | $stack->setHandler(new CurlHandler()); 26 | $logger = $this->create($accessToken); 27 | $stack->push($logger); 28 | 29 | return $stack; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Kobo/Request/Bookmark.php: -------------------------------------------------------------------------------- 1 | $readingStates 9 | */ 10 | public function __construct(public array $readingStates = []) 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Kobo/Request/TagDeleteRequest.php: -------------------------------------------------------------------------------- 1 | */ 10 | public array $items; 11 | 12 | public function hasItem(Book $book): bool 13 | { 14 | foreach ($this->items as $item) { 15 | if ($item->revisionId === $book->getUuid() 16 | && $item->type === TagDeleteRequestItem::TYPE_REVISION_TAG_ITEM) { 17 | return true; 18 | } 19 | } 20 | 21 | return false; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Kobo/Request/TagDeleteRequestItem.php: -------------------------------------------------------------------------------- 1 | setRevisionId($revisionId); 11 | } 12 | } 13 | 14 | public const TYPE_REVISION_TAG_ITEM = 'ProductRevisionTagItem'; 15 | 16 | public ?string $revisionId = null; 17 | public ?string $type = self::TYPE_REVISION_TAG_ITEM; 18 | 19 | public function setRevisionId(?string $revisionId): self 20 | { 21 | $this->revisionId = $revisionId; 22 | 23 | return $this; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Kobo/Response/ReadingStateResponseFactory.php: -------------------------------------------------------------------------------- 1 | bookProgressionService, 27 | $this->serializer, 28 | $syncToken, 29 | $koboDevice, 30 | $book 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Kobo/Response/StateResponse.php: -------------------------------------------------------------------------------- 1 | $isSuccess ? 'Success' : 'FailedCommands', 15 | 'UpdateResults' => [ 16 | [ 17 | 'CurrentBookmarkResult' => [ 18 | 'Result' => 'Success', 19 | ], 20 | 'EntitlementId' => $bookOrUuid instanceof Book ? $bookOrUuid->getUuid() : $bookOrUuid, 21 | 'StatisticsResult' => [ 22 | 'Result' => 'Success', 23 | ], 24 | 'StatusInfoResult' => [ 25 | 'Result' => $isSuccess ? 'Success' : 'Conflict', 26 | ], 27 | ], 28 | ], 29 | ], Response::HTTP_OK); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Kobo/Serializer/KoboNameConverter.php: -------------------------------------------------------------------------------- 1 | PHP entitlementId 11 | * 12 | * @param class-string|null $class 13 | * @param array $context 14 | */ 15 | #[\Override] 16 | public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string 17 | { 18 | return ucfirst($propertyName); 19 | } 20 | 21 | /** 22 | * PHP entitlementId => JSON EntitlementId 23 | * 24 | * @param class-string|null $class 25 | * @param array $context 26 | */ 27 | #[\Override] 28 | public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string 29 | { 30 | return lcfirst($propertyName); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Repository/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/src/Repository/.gitignore -------------------------------------------------------------------------------- /src/Repository/AiModelRepository.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class AiModelRepository extends ServiceEntityRepository 13 | { 14 | public function __construct(ManagerRegistry $registry) 15 | { 16 | parent::__construct($registry, AiModel::class); 17 | } 18 | 19 | public function findAllIndexed(): array 20 | { 21 | $allModels = $this->findAll(); 22 | $models = []; 23 | foreach ($allModels as $model) { 24 | $models[$model->getId()] = $model; 25 | } 26 | 27 | return $models; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Repository/BookmarkUserRepository.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class BookmarkUserRepository extends ServiceEntityRepository 13 | { 14 | public function __construct(ManagerRegistry $registry) 15 | { 16 | parent::__construct($registry, BookmarkUser::class); 17 | } 18 | 19 | // /** 20 | // * @return BookmarkUser[] Returns an array of BookmarkUser objects 21 | // */ 22 | // public function findByExampleField($value): array 23 | // { 24 | // return $this->createQueryBuilder('b') 25 | // ->andWhere('b.exampleField = :val') 26 | // ->setParameter('val', $value) 27 | // ->orderBy('b.id', 'ASC') 28 | // ->setMaxResults(10) 29 | // ->getQuery() 30 | // ->getResult() 31 | // ; 32 | // } 33 | 34 | // public function findOneBySomeField($value): ?BookmarkUser 35 | // { 36 | // return $this->createQueryBuilder('b') 37 | // ->andWhere('b.exampleField = :val') 38 | // ->setParameter('val', $value) 39 | // ->getQuery() 40 | // ->getOneOrNullResult() 41 | // ; 42 | // } 43 | } 44 | -------------------------------------------------------------------------------- /src/Repository/InstanceConfigurationRepository.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InstanceConfigurationRepository extends ServiceEntityRepository 13 | { 14 | public function __construct(ManagerRegistry $registry) 15 | { 16 | parent::__construct($registry, InstanceConfiguration::class); 17 | } 18 | 19 | public function save(InstanceConfiguration $entity, bool $flush = false): void 20 | { 21 | $this->getEntityManager()->persist($entity); 22 | 23 | if ($flush) { 24 | $this->getEntityManager()->flush(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Repository/OpdsAccessRepository.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class OpdsAccessRepository extends ServiceEntityRepository 13 | { 14 | public function __construct(ManagerRegistry $registry) 15 | { 16 | parent::__construct($registry, OpdsAccess::class); 17 | } 18 | 19 | public function findOneByToken(string $token): ?OpdsAccess 20 | { 21 | $qb = $this->createQueryBuilder('oa'); 22 | $qb->where('oa.token = :token'); 23 | $qb->setParameter('token', $token); 24 | $qb->join('oa.user', 'u'); 25 | 26 | return $this->findOneBy(['token' => $token]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Security/Badge/KoboDeviceBadge.php: -------------------------------------------------------------------------------- 1 | device; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Security/KoboAccessTokenHandlerInterface.php: -------------------------------------------------------------------------------- 1 | repository->byAccessKey($token); 27 | if (!$koboDevice instanceof KoboDevice) { 28 | throw new BadCredentialsException('Invalid credentials.'); 29 | } 30 | 31 | return new KoboDeviceBadge($koboDevice); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Security/OpdsTokenExtractor.php: -------------------------------------------------------------------------------- 1 | getRequestUri(); 18 | 19 | return $this->extractAccessTokenFromUri($uri); 20 | } 21 | 22 | public function extractAccessTokenFromServerRequest(ServerRequestInterface $request): ?string 23 | { 24 | $uri = $request->getUri()->getPath(); 25 | 26 | return $this->extractAccessTokenFromUri($uri); 27 | } 28 | 29 | public function getOriginalPath(ServerRequestInterface $request, string $path): string 30 | { 31 | $token = $this->extractAccessTokenFromServerRequest($request); 32 | if ($token === null) { 33 | return $path; 34 | } 35 | 36 | return str_replace('/opds/'.$token, '', $path); 37 | } 38 | 39 | private function extractAccessTokenFromUri(string $uri): ?string 40 | { 41 | $uri = explode('/', $uri); 42 | if (count($uri) >= 3 && $uri[1] === 'opds') { 43 | return $uri[2]; 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Security/OpdsTokenHandler.php: -------------------------------------------------------------------------------- 1 | repository->findOneByToken($accessToken); 27 | 28 | if (!$accessTokenObject instanceof OpdsAccess) { 29 | throw new BadCredentialsException('Invalid credentials.'); 30 | } 31 | $user = $accessTokenObject->getUser(); 32 | if (!$user instanceof User) { 33 | throw new BadCredentialsException('Invalid credentials.'); 34 | } 35 | 36 | return new UserBadge($user->getUserIdentifier()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Security/Token/PostAuthenticationTokenWithKoboDevice.php: -------------------------------------------------------------------------------- 1 | device; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Security/Voter/BookInteractionVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 22 | // if the user is anonymous, do not grant access 23 | if (!$user instanceof UserInterface) { 24 | return false; 25 | } 26 | 27 | if (!$subject instanceof BookInteraction) { 28 | return false; 29 | } 30 | 31 | return $subject->getUser() === $user; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Security/Voter/BookVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 28 | // if the user is anonymous, do not grant access 29 | if (!$user instanceof User) { 30 | return false; 31 | } 32 | 33 | if (!$subject instanceof Book) { 34 | return false; 35 | } 36 | 37 | return match ($attribute) { 38 | self::EDIT => in_array('ROLE_ADMIN', $user->getRoles(), true), 39 | self::VIEW, self::DOWNLOAD => !$user->getMaxAgeCategory() instanceof AgeCategory || ($subject->getAgeCategory()->value ?? 0) <= ($user->getMaxAgeCategory()->value ?? 999), 40 | default => false, 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Security/Voter/KoboDeviceVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 28 | // if the user is anonymous, do not grant access 29 | if (!$user instanceof UserInterface) { 30 | return false; 31 | } 32 | 33 | if (!$subject instanceof KoboDevice) { 34 | return false; 35 | } 36 | 37 | if ($subject->getUser() === $user) { 38 | return true; 39 | } 40 | 41 | return in_array('ROLE_ADMIN', $user->getRoles(), true); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Security/Voter/RelocationVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 30 | 31 | if ($this->allowBookRelocation && PHP_SAPI === 'cli') { 32 | return true; 33 | } 34 | // if the user is anonymous, do not grant access 35 | if (!$user instanceof UserInterface) { 36 | return false; 37 | } 38 | 39 | if (!$this->allowBookRelocation) { 40 | return false; 41 | } 42 | 43 | return in_array('ROLE_ADMIN', $user->getRoles(), true); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Service/BookInteractionService.php: -------------------------------------------------------------------------------- 1 | getLastInteraction($user); 23 | if ($interaction !== null) { 24 | if ($interaction->getReadStatus() === ReadStatus::Finished) { 25 | $readBooks++; 26 | } elseif ($interaction->getReadStatus() === ReadStatus::Started) { 27 | $inProgressBooks++; 28 | } 29 | 30 | if ($interaction->getReadingList() === ReadingList::Ignored) { 31 | $hiddenBooks++; 32 | } 33 | } 34 | } 35 | 36 | return [ 37 | 'readBooks' => $readBooks, 38 | 'hiddenBooks' => $hiddenBooks, 39 | 'inProgressBooks' => $inProgressBooks, 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Service/ConnectionKeepAliveSubscriber.php: -------------------------------------------------------------------------------- 1 | isMainRequest()) { 14 | return; 15 | } 16 | 17 | $version = $event->getRequest()->getProtocolVersion(); 18 | 19 | // Keep alive is only supported in HTTP/1.0 and HTTP/1.1 20 | if (false === in_array($version, ['HTTP/1.0', 'HTTP/1.1'], true)) { 21 | return; 22 | } 23 | 24 | // If the controller already set the Connection header, we don't want to override it 25 | if ($event->getResponse()->headers->has('Connection')) { 26 | return; 27 | } 28 | 29 | // We add the Connection: keep-alive header if the client requested it 30 | $connection = $event->getRequest()->headers->get('Connection'); 31 | if ($connection === 'keep-alive') { 32 | $event->getResponse()->headers->set('Connection', 'keep-alive'); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Service/FilteredBookUrlGenerator.php: -------------------------------------------------------------------------------- 1 | $value) { 11 | if (!is_array($value)) { 12 | $value = [$value]; 13 | } 14 | foreach ($value as $v) { 15 | $fullQuery .= $key.':=`'.$v.'` '; 16 | } 17 | } 18 | 19 | return ['filterQuery' => $fullQuery]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Service/ShelfManager.php: -------------------------------------------------------------------------------- 1 | getQueryString() === null && $shelf->getQueryFilter() === null) { 17 | return $shelf->getBooks()->toArray(); 18 | } 19 | 20 | $this->searchHelper->prepareQuery( 21 | $shelf->getQueryString() ?? '*', 22 | $shelf->getQueryFilter() ?? '', 23 | $shelf->getQueryOrder() ?? '', 24 | 200 25 | )->execute(); 26 | 27 | return $this->searchHelper->getBooks(); 28 | } 29 | 30 | public function getBooksInShelves(array $shelves): array 31 | { 32 | $books = []; 33 | foreach ($shelves as $shelf) { 34 | // TODO: Use multi-search (when ready) to avoid many queries 35 | $shelfBooks = $this->getBooksInShelf($shelf); 36 | foreach ($shelfBooks as $book) { 37 | $books[$book->getId()] = $book; 38 | } 39 | } 40 | 41 | return $books; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Service/ThemeSelector.php: -------------------------------------------------------------------------------- 1 | getTheme() === 'dark'; 18 | } 19 | 20 | public function getTheme(): ?string 21 | { 22 | $user = $this->security->getUser(); 23 | 24 | if ($user instanceof User) { 25 | return $user->getTheme(); 26 | } 27 | 28 | return null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Twig/Components/BootstrapModal.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Twig\Components; 13 | 14 | use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; 15 | 16 | #[AsTwigComponent] 17 | class BootstrapModal 18 | { 19 | public ?string $id = null; 20 | 21 | public function __invoke(): void 22 | { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Twig/Components/InlineEditVerified.php: -------------------------------------------------------------------------------- 1 | entityManager->flush(); 37 | $this->dispatchBrowserEvent('manager:flush'); 38 | $this->flashMessage = ' book updated'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Twig/Extension/ThemeExtension.php: -------------------------------------------------------------------------------- 1 | themeSelector->getTheme(); 17 | 18 | if ($theme !== null) { 19 | return ['themes/'.$theme.'/'.$value, $value]; 20 | } 21 | 22 | return $value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Twig/UniqueIdExtension.php: -------------------------------------------------------------------------------- 1 | uniqueID(...)), 18 | ]; 19 | } 20 | 21 | public function uniqueID(string $prefix = '', bool $entropy = false): string 22 | { 23 | return uniqid($prefix, $entropy); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /templates/ai_model/_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form) }} 2 |
3 |
4 |
5 |
6 | {{ form_widget(form) }} 7 | 8 |
9 |
10 |
11 |
12 | {{ form_end(form) }} 13 | -------------------------------------------------------------------------------- /templates/ai_model/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}{{ "ai.edit-ai-model"|trans }}{% endblock %} 4 | 5 | {% block body %} 6 | 7 | 8 | {{ "generic.back-to-list"|trans }} 9 | 10 | 11 | {{ include('ai_model/_form.html.twig', {'button_label': 'generic.update'}) }} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/ai_model/new.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}{{ "ai.new-aimodel"|trans }}{% endblock %} 4 | 5 | {% block body %} 6 | 7 | 8 | {{ "generic.back-to-list"|trans }} 9 | 10 | {{ include('ai_model/_form.html.twig') }} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/bare.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | {{ block('title')|striptags }} - Biblioteca 10 | 11 | {% block stylesheets %} 12 | {{ encore_entry_link_tags('app') }} 13 | {% endblock %} 14 | 15 | 16 | {% block content %}{% endblock %} 17 | 18 | {% block javascripts %} 19 | {{ encore_entry_script_tags('app') }} 20 | {% endblock %} 21 | 22 | 23 | -------------------------------------------------------------------------------- /templates/book/_book-row-empty.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ index }} 4 | 5 | 6 | {% include themedTemplate('book/_cover_empty.html.twig') %} 7 | 8 | 9 |

{{ "book.missing"|trans }}

10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /templates/book/_book-row.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ index }} 4 | 5 | 6 | 7 | 8 | {% include themedTemplate('book/_cover.html.twig') with { book: book } %} 9 | 10 | 11 | 12 |

{{ book.title }}

13 | {% if book.authors|length > 0%} 14 |
15 | {% for author in book.authors %} 16 | 17 |  {{ author }} 18 | 19 | {% endfor %} 20 |
21 | {% endif %} 22 | {% set interaction = book.lastInteraction(app.user) %} 23 | {% if interaction is not null and interaction.rating > 0 %} 24 | {% include themedTemplate('components/_rating.html.twig') with {rating: interaction.rating} %} 25 | {% endif %} 26 | 27 | 28 | {{ component('InlineEditInteraction', {book:book}) }} 29 | 30 | 31 | -------------------------------------------------------------------------------- /templates/book/_cover_empty.html.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | -------------------------------------------------------------------------------- /templates/book/_empty.html.twig: -------------------------------------------------------------------------------- 1 | {# @var book App\Entity\Book #} 2 |
3 | {% include themedTemplate('book/_cover_empty.html.twig') %} 4 | 5 | {{ "book.missing"|trans }}
6 | {% if serie %} 7 | 8 |  {{ serie }} ({{ index }}) 9 | 10 | 11 | {% endif %} 12 |
13 |
-------------------------------------------------------------------------------- /templates/book/reader-files.html.twig: -------------------------------------------------------------------------------- 1 | {% extends themedTemplate('bare.html.twig') %} 2 | 3 | {% block title %}{{ book.authors|first }} {% if book.serie is not null %}> {{ book.serie }} #{{ book.serieIndex }} {% endif %}> {{ book.title }}{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 | 13 |
14 | {{ knp_pagination_render(pagination) }} 15 |
16 |
17 | 18 | {% for file in pagination %} 19 |
20 | 21 | 22 | 23 |
24 | {% endfor %} 25 |
26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/book/upload.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | 4 | {% block title %} 5 | {{ "book.upload-books-to-the-consume-folder"|trans }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 |
10 |
11 | {{ form(form) }} 12 |
13 |
14 | {% endblock %} 15 | 16 | -------------------------------------------------------------------------------- /templates/bundles/TwigBundle/Exception/error.html.twig: -------------------------------------------------------------------------------- 1 | {# templates/bundles/TwigBundle/Exception/error404.html.twig #} 2 | {% extends 'base.html.twig' %} 3 | 4 | {% block title %}{{ status_text }}{% endblock %} 5 | {% block body %} 6 |

{{ "error.error-happened"|trans }}

7 |

{{ status_code }} {{ status_text }}

8 |

{{exception.message}}

9 |
10 | {{ "error.stack-trace"|trans }} 11 | {{ exception.traceAsString }} 12 |
13 | 14 | {% endblock %} -------------------------------------------------------------------------------- /templates/components/AddNewShelf.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {% if isEditing %} 3 |
4 |
5 | 11 | 18 |
19 |
20 | {% else %} 21 | 22 | 31 | 32 | {% endif %} 33 |
34 | -------------------------------------------------------------------------------- /templates/components/BootstrapModal.html.twig: -------------------------------------------------------------------------------- 1 |
7 | 31 |
32 | -------------------------------------------------------------------------------- /templates/components/FieldGuesser.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {% if this.guessAuthor!='' %} 3 |
4 | 16 |
17 | {% endif %} 18 | {% if this.guessIndex!='' and this.guessIndex!= book.serieIndex %} 19 |
20 | 28 | 36 |
37 | {% endif %} 38 |
39 | -------------------------------------------------------------------------------- /templates/components/InlineEditMultiple.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {% if isEditing and is_granted('ROLE_ADMIN') %} 3 |
4 | 13 | 20 |
21 | {% elseif is_granted('ROLE_ADMIN') %} 22 | 23 | 32 | 33 | {% if flashMessage %} 34 | 35 | {% endif %} 36 | {% endif %} 37 | 38 |
39 | -------------------------------------------------------------------------------- /templates/components/InlineEditVerified.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {% if is_granted('EDIT', book) %} 3 |
4 |
5 | 12 | 13 |
14 |
15 | {% endif %} 16 | {% if flashMessage %} 17 | 18 | {% endif %} 19 |
20 | -------------------------------------------------------------------------------- /templates/components/UploadBookPicture.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {% if isEditing and is_granted('EDIT', book) %} 3 |
4 | 5 | 6 | 9 |
10 | {% elseif is_granted('EDIT', book) %} 11 | 12 | 21 | 22 | {% endif %} 23 | {% if flashMessage %} 24 |
25 | {{ flashMessage }} 26 |
27 | {% endif %} 28 |
29 | -------------------------------------------------------------------------------- /templates/components/_facet.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {% set value = facet.field_name ~':=`'~ count.value ~'`' %} 3 | {% set label = count.value %} 4 | 5 | {% set isIncluded = value in filterQuery %} 6 | 27 | -------------------------------------------------------------------------------- /templates/components/_rating.html.twig: -------------------------------------------------------------------------------- 1 |
2 | {% for current in 1..rating %} 3 | 4 | {% endfor %} 5 |
-------------------------------------------------------------------------------- /templates/default/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends themedTemplate('base.html.twig') %} 2 | 3 | {% block title %} {{ "book.all-books"|trans }} {% endblock %} 4 | {% block body %} 5 | 6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /templates/default/readingList.html.twig: -------------------------------------------------------------------------------- 1 | {% extends themedTemplate('base.html.twig') %} 2 | 3 | {% block title %} 4 | {{ 'reading-list.title'|trans }} 5 | {% endblock %} 6 | 7 | {% block body %} 8 | 9 | {% if readlist.unread|length>0 %} 10 |

{{ 'reading-list.to-read'|trans }}

11 |
12 | {% for book in readlist.unread %} 13 |
14 | {% include themedTemplate('book/_teaser.html.twig') with {book: book} %} 15 |
16 | {% endfor %} 17 |
18 | {% else %} 19 |
20 | {{ 'reading-list.add-to-favorites'|trans }} 21 |
22 | {% endif %} 23 | 24 | {% if readlist.finished|length>0 %} 25 |

{{ 'reading-list.finished'|trans }}

26 |
27 | {% for book in readlist.finished %} 28 |
29 | {% include themedTemplate('book/_teaser.html.twig') with {book: book} %} 30 |
31 | {% endfor %} 32 |
33 | {% endif %} 34 | 35 | {% endblock %} 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /templates/default/timeline.html.twig: -------------------------------------------------------------------------------- 1 | {% extends themedTemplate('base.html.twig') %} 2 | 3 | {% block title %}{{ "timeline.title"|trans }}{% endblock %} 4 | {% block body %} 5 | 6 |
7 | {% for availableYear in years %} 8 | {{ availableYear??'None' }} 9 | {% endfor %} 10 |
11 | 12 |
13 | All 14 | {% for availableType in types %} 15 | {{ availableType }} 16 | {% endfor %} 17 |
18 | 19 |

{{ "timeline.timeline-for"|trans }} {{ year }}: {{ books|length }} {{ type }}

20 |
21 | {% for book in books %} 22 |
23 | {% include themedTemplate('book/_teaser.html.twig') %} 24 |
25 | {% endfor %} 26 |
{% endblock %} 27 | -------------------------------------------------------------------------------- /templates/instance_configuration/_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form) }} 2 | {{ form_widget(form) }} 3 | 4 | {{ form_end(form) }} 5 | -------------------------------------------------------------------------------- /templates/instance_configuration/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}{{ "instance-configuration.edit-configuration"|trans }}{% endblock %} 4 | 5 | {% block body %} 6 | 7 | 8 | 9 | {{ "generic.back-to-list"|trans }} 10 | 11 | 12 | {{ include('instance_configuration/_form.html.twig', {'button_label': 'generic.update'|trans}) }} 13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/instance_configuration/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}{{ "instance-configuration.title"|trans }}{% endblock %} 4 | 5 | {% block body %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {%for key, param in documentedParams%} 14 | 15 | 16 | 17 | 18 | 19 | {%endfor%} 20 | {%for key, param in editableParams%} 21 | 22 | 23 | 24 | 28 | 29 | {%endfor%} 30 |
{{ "instance-configuration.parameter"|trans }}{{ "instance-configuration.description"|trans }}{{ "instance-configuration.current-value"|trans }}
{{key}}{{param.description}}{{param.value}}
{{key}}{{ "instance-configuration.this-value-can-be-set-from-dotenv-or-from-this-page"|trans }} 25 | {{param}} 26 | {{ "instance-configuration.edit"|trans }} 27 |
31 | 32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/kobodevice_user/_delete_form.html.twig: -------------------------------------------------------------------------------- 1 | {% if is_granted("DELETE", kobo) %} 2 |
3 | 4 | 5 |
6 | {% endif %} -------------------------------------------------------------------------------- /templates/kobodevice_user/_form.html.twig: -------------------------------------------------------------------------------- 1 | {% form_theme form 'kobodevice_user/_form_theme.html.twig' %} 2 | {{ form_start(form) }} 3 | {{ form_widget(form) }} 4 | 5 | {{ form_end(form) }} 6 | -------------------------------------------------------------------------------- /templates/kobodevice_user/_form_theme.html.twig: -------------------------------------------------------------------------------- 1 | {% block _kobo_lastSyncToken_widget %} 2 | {{ block('form_widget_simple') }} 3 | 4 | {% if form.parent.vars.data.id|default(false) and data is not null %} 5 | 6 | {{ 'kobo.form.reset-sync-token'|trans }} 7 | 8 | {% endif %} 9 | {% endblock %} -------------------------------------------------------------------------------- /templates/kobodevice_user/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}{{ "kobo.device.edit-kobo"|trans }}{% endblock %} 4 | 5 | {% block body %} 6 | {{ include('kobodevice_user/_form.html.twig', {'button_label': 'generic.update'}) }} 7 | 8 |
9 | {{ include('kobodevice_user/_delete_form.html.twig') }} 10 |
11 | {{ include('kobodevice_user/instructions.html.twig', { token: kobo.accessKey, open:true }) }} 12 | 13 | {% endblock %} 14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/kobodevice_user/logs.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}{{ "kobo.logs.title"|trans }}{% endblock %} 4 | 5 | {% block body %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for record in records %} 17 | 18 | 21 | 24 | 27 | 33 | 34 | {% endfor %} 35 | 36 |
{{ "kobo.logs.date"|trans }}{{ "kobo.logs.device"|trans }}{{ "kobo.logs.channel"|trans }}{{ "kobo.logs.message"|trans }}
19 | {{ record.datetime|date('d.m.Y') }} {{ record.datetime|date('H:i:s') }} 20 | 22 | {{ record.extra.kobo|default('unkown') }} 23 | 25 | {{ record.channel }} 26 | 28 |
29 | {{ record.message }} 30 |
{{ record.context|json_encode(constant('JSON_PRETTY_PRINT')) }}
31 |
32 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /templates/kobodevice_user/new.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}{{ "kobo.device.create-new"|trans }}{% endblock %} 4 | 5 | {% block body %} 6 | 7 | {{ include('kobodevice_user/_form.html.twig') }} 8 | 9 | {{ include('kobodevice_user/instructions.html.twig') }} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /templates/shelf/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends themedTemplate('base.html.twig') %} 2 | 3 | {% block title %}{{ shelf.name }}{% endblock %} 4 | 5 | {% block body %} 6 | 7 |
8 | {% if books|length==200 %} 9 |
10 |
11 | {{ 'shelf.max-number-of-books'|trans }} 12 |
13 |
14 | {% endif %} 15 | {% for book in books %} 16 |
17 | {% include themedTemplate('book/_teaser.html.twig') with {book: book} %} 18 |
19 | {% endfor %} 20 |
21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/shelf_crud/_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form) }} 2 | {{ form_widget(form) }} 3 | 4 | {{ form_end(form) }} 5 | -------------------------------------------------------------------------------- /templates/shelf_crud/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}{{ "shelf.edit-shelf"|trans }}{% endblock %} 4 | 5 | {% block body %} 6 | {{ include('shelf_crud/_form.html.twig', {'button_label': 'generic.update'}) }} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /templates/themes/dark/bare.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | {{ block('title')|striptags }} - Biblioteca 10 | 11 | {% block stylesheets %} 12 | {{ encore_entry_link_tags('app') }} 13 | {% endblock %} 14 | 15 | 16 | {% block content %}{% endblock %} 17 | 18 | {% block javascripts %} 19 | {{ encore_entry_script_tags('app') }} 20 | {% endblock %} 21 | 22 | 23 | -------------------------------------------------------------------------------- /templates/user/_delete_form.html.twig: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /templates/user/_form.html.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | {{ form_start(form) }} 6 | {{ form_widget(form) }} 7 | 8 | {{ form_end(form) }} 9 |
10 |
11 |
12 |
-------------------------------------------------------------------------------- /templates/user/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends themedTemplate('base.html.twig') %} 2 | 3 | {% block title %}{{ "user.edit-user"|trans }}{% endblock %} 4 | 5 | {% block body %} 6 | {{ include('user/_form.html.twig', {'button_label': 'generic.update'}) }} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /templates/user/new.html.twig: -------------------------------------------------------------------------------- 1 | {% extends themedTemplate('base.html.twig') %} 2 | 3 | {% block title %}{{ "user.new-user"|trans }}{% endblock %} 4 | 5 | {% block body %} 6 | {{ include('user/_form.html.twig') }} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /tests/Command/CreateUserCommandTest.php: -------------------------------------------------------------------------------- 1 | find('app:create-admin-user'); 20 | $commandTester = new CommandTester($command); 21 | $commandTester->execute([ 22 | 'username' => 'test', 23 | 'password' => 'test', 24 | ]); 25 | 26 | $commandTester->assertCommandIsSuccessful(); 27 | 28 | $output = $commandTester->getDisplay(); 29 | self::assertStringContainsString('User created', $output); 30 | 31 | $userRepository = static::getContainer()->get(UserRepository::class); 32 | 33 | self::assertInstanceOf(UserRepository::class, $userRepository); 34 | 35 | $user = $userRepository->findOneBy(['username' => 'test']); 36 | 37 | self::assertNotNull($user); 38 | 39 | $userRepository->remove($user, true); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Controller/Kobo/Api/V1/GenericControllerTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/kobo/'.KoboFixture::ACCESS_KEY.'/v1/products/deals'); 15 | 16 | self::assertResponseStatusCodeSame(307); 17 | self::assertResponseHeaderSame('Location', 'http://storeapi.kobo.com/v1/products/deals'); 18 | } 19 | 20 | public function testBenefits(): void 21 | { 22 | $client = self::getClient(); 23 | $client?->request('GET', '/kobo/'.KoboFixture::ACCESS_KEY.'/v1/user/loyalty/benefits'); 24 | self::assertResponseIsSuccessful(); 25 | self::assertThat(self::getJsonResponse(), new JSONContainKeys(['Benefits']), 'Response does not contain all expected keys'); 26 | } 27 | 28 | public function testBenefitsWithProxy(): void 29 | { 30 | $this->enableRemoteSync(); 31 | 32 | $client = self::getClient(); 33 | $client?->request('GET', '/kobo/'.KoboFixture::ACCESS_KEY.'/v1/user/loyalty/benefits'); 34 | self::assertResponseStatusCodeSame(307); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Controller/Kobo/Api/V1/ProductsControllerTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/kobo/'.KoboFixture::ACCESS_KEY.'/v1/products/'.$unknownUuid.'/nextread'); 17 | self::assertResponseIsSuccessful(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Controller/Kobo/Api/V3/ContentControllerTest.php: -------------------------------------------------------------------------------- 1 | request('POST', '/api/v3/content/checkforchanges'); 15 | self::assertResponseIsSuccessful(); 16 | 17 | /** @var Response|null $response */ 18 | $response = $client?->getResponse(); 19 | 20 | $responseContent = $response?->getContent(); 21 | self::assertSame('[]', $responseContent); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/DynamicShelfTest.php: -------------------------------------------------------------------------------- 1 | get('doctrine'); 17 | 18 | /** @var EntityManagerInterface $entityManager */ 19 | $entityManager = $doctrine->getManager(); 20 | 21 | $userRepository = static::getContainer()->get(UserRepository::class); 22 | 23 | self::assertInstanceOf(UserRepository::class, $userRepository); 24 | 25 | $testUser = $userRepository->findOneBy(['username' => 'admin@example.com']); 26 | 27 | self::assertInstanceOf(UserInterface::class, $testUser); 28 | 29 | $client->loginUser($testUser); 30 | $shelf = new Shelf(); 31 | $shelf->setName('test-dyn'); 32 | $shelf->setQueryString('*'); 33 | $shelf->setUser($testUser); 34 | $entityManager->persist($shelf); 35 | $entityManager->flush(); 36 | 37 | $client->request(Request::METHOD_GET, '/shelf/test-dyn'); 38 | self::assertResponseIsSuccessful(); 39 | 40 | self::assertSelectorExists('.book'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/FileSystemManagerForTests.php: -------------------------------------------------------------------------------- 1 | get('doctrine.orm.entity_manager'); 21 | 22 | $book = new Book(); 23 | $book->setTitle('test'); 24 | $book->setChecksum(md5('test')); 25 | $book->setBookPath('test'); 26 | $book->setBookFilename('test'); 27 | $book->setExtension('ebook'); 28 | 29 | $entityManager->persist($book); 30 | $entityManager->flush(); 31 | 32 | self::assertNotNull($book->getUpdated(), 'The updated date should be set by Doctrine Extensions'); 33 | 34 | $entityManager->remove($book); 35 | $entityManager->flush(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Kobo/DownloadHelperTest.php: -------------------------------------------------------------------------------- 1 | getBookFileName(); 21 | self::assertFileExists($assetPath, 'Asset not found'); 22 | $size = (int) filesize($assetPath); 23 | $downloadHelper = $this->getService(DownloadHelper::class); 24 | self::assertGreaterThan(0, $size, 'invalid file size'); 25 | self::assertSame($size, $downloadHelper->getSize($this->getBook()), 'invalid file size'); 26 | } 27 | 28 | public function testExists(): void 29 | { 30 | $assetPath = $this->getBookFileName(); 31 | self::assertFileExists($assetPath, 'Asset not found'); 32 | 33 | $downloadHelper = $this->getService(DownloadHelper::class); 34 | self::assertTrue($downloadHelper->exists($this->getBook()), 'book file should exists'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Kobo/Proxy/KoboProxyConfigurationTest.php: -------------------------------------------------------------------------------- 1 | getService(KoboProxyConfiguration::class)->getNativeInitializationJson(); 16 | self::assertArrayHasKey('image_url_template', $data['Resources']); 17 | $imageTemplate = $data['Resources']['image_url_template']; 18 | self::assertIsString($imageTemplate); 19 | self::assertStringContainsString('/{ImageId}/{Width}/{Height}/false/image.jpg', $imageTemplate); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Kobo/Request/ReadingStateDeserializeTest.json: -------------------------------------------------------------------------------- 1 | { 2 | "ReadingStates": [ 3 | { 4 | "CurrentBookmark": { 5 | "ContentSourceProgressPercent": 33, 6 | "LastModified": "2024-01-21T09:26:53Z", 7 | "Location": { 8 | "Source": "GoogleDoc/363Citationsinspirantespourpasserlaction.xhtml", 9 | "Type": "KoboSpan", 10 | "Value": "kobo.409.3" 11 | }, 12 | "ProgressPercent": 34 13 | }, 14 | "EntitlementId": "52e5beac-3705-490f-8cfc-fa37190cb92b", 15 | "LastModified": "2024-01-21T09:26:53Z", 16 | "Statistics": { 17 | "LastModified": "2024-01-21T09:26:53Z", 18 | "RemainingTimeMinutes": 31, 19 | "SpentReadingMinutes": 71 20 | }, 21 | "StatusInfo": { 22 | "LastModified": "2024-01-21T09:26:53Z", 23 | "Status": "Reading" 24 | } 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /tests/LoginTest.php: -------------------------------------------------------------------------------- 1 | request(Request::METHOD_GET, '/'); 16 | 17 | self::assertResponseRedirects('/login'); 18 | $client->followRedirect(); 19 | 20 | $client->submitForm('Sign in', ['_username' => UserFixture::USER_USERNAME, '_password' => UserFixture::USER_PASSWORD]); 21 | 22 | $client->followRedirect(); 23 | 24 | $client->request(Request::METHOD_GET, '/'); 25 | self::assertResponseIsSuccessful(); 26 | 27 | self::assertSelectorTextContains('h1', 'Aloha '.UserFixture::USER_USERNAME.'!'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Resources/books/fake-AChristmasCarol.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-AChristmasCarol.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-AnnaKarenina.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-AnnaKarenina.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-BrothersKaramazov.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-BrothersKaramazov.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-CountOfMonteCristo.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-CountOfMonteCristo.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-CrimeAndPunishment.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-CrimeAndPunishment.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-DivineComedy.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-DivineComedy.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-DonQuixote.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-DonQuixote.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-DorianGray.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-DorianGray.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-Dracula.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-Dracula.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-Frankenstein.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-Frankenstein.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-GreatExpectations.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-GreatExpectations.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-JaneEyre.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-JaneEyre.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-LesMiserables.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-LesMiserables.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-MobyDick.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-MobyDick.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-PrideAndPrejudice.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-PrideAndPrejudice.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-SherlockHolmes.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-SherlockHolmes.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-TaleOfTwoCities.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-TaleOfTwoCities.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-TheJungleBook.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-TheJungleBook.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-WarAndPeace.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-WarAndPeace.epub -------------------------------------------------------------------------------- /tests/Resources/books/fake-WutheringHeights.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/fake-WutheringHeights.epub -------------------------------------------------------------------------------- /tests/Resources/books/real-TheOdysses.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/books/real-TheOdysses.epub -------------------------------------------------------------------------------- /tests/Resources/covers/TheOdysses.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/Resources/covers/TheOdysses.jpg -------------------------------------------------------------------------------- /tests/SampleTest.php: -------------------------------------------------------------------------------- 1 | setTime($this->now()->modify($diff)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 11 | } 12 | -------------------------------------------------------------------------------- /tests/coverage/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/tests/coverage/.gitkeep -------------------------------------------------------------------------------- /translations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biblioverse/biblioteca/ffd39a0a278e30ed1b918dffe805dce64b24c06b/translations/.gitignore -------------------------------------------------------------------------------- /translations/AutocompleteBundle.en.yaml: -------------------------------------------------------------------------------- 1 | 'Loading more results...': 'Loading more results...' 2 | 'No results found': 'No results found' 3 | 'No more results': 'No more results' 4 | 'Add %placeholder%...': 'Add %placeholder%...' 5 | -------------------------------------------------------------------------------- /translations/AutocompleteBundle.fr.yaml: -------------------------------------------------------------------------------- 1 | 'Loading more results...': "Chargement d'autres résultats..." 2 | 'No results found': 'Aucun résultat trouvé' 3 | 'No more results': 'Aucun autre résultat trouvé' 4 | 'Add %placeholder%...': 'Ajouter %placeholder%...' 5 | -------------------------------------------------------------------------------- /translations/KnpPaginatorBundle.en.yaml: -------------------------------------------------------------------------------- 1 | label_previous: Previous 2 | label_next: Next 3 | filter_searchword: Searchword... 4 | -------------------------------------------------------------------------------- /translations/KnpPaginatorBundle.fr.yaml: -------------------------------------------------------------------------------- 1 | label_previous: Précédent 2 | label_next: Suivant 3 | filter_searchword: Recherche... 4 | --------------------------------------------------------------------------------