diff --git a/.env b/.env index 6ceaf7f..4adb167 100644 --- a/.env +++ b/.env @@ -49,3 +49,7 @@ MERCURE_PUBLIC_URL=https://localhost/.well-known/mercure # The secret used to sign the JWTs MERCURE_JWT_SECRET="Mangarr-JWT-Secret" ###< symfony/mercure-bundle ### + +#Custom +MANGA_DATA_PATH=/mnt/c/Users/jerem/Mangas +IMAGE_DATA_PATH=/mnt/c/Users/jerem/MangasImages diff --git a/assets/controllers/scrapper_configure_controller.js b/assets/controllers/scrapper_configure_controller.js index c86c690..ad157d1 100644 --- a/assets/controllers/scrapper_configure_controller.js +++ b/assets/controllers/scrapper_configure_controller.js @@ -7,13 +7,11 @@ export default class extends Controller { } async saveConfiguration(event) { - console.log('saveConfiguration called'); event.preventDefault(); this.formTarget.submit(); } async testConfiguration(event) { - console.log('testConfiguration called'); event.preventDefault(); const formData = new FormData(this.formTarget); const testFormData = new FormData(this.testFormTarget); diff --git a/assets/controllers/scrapper_import_controller.js b/assets/controllers/scrapper_import_controller.js new file mode 100644 index 0000000..98b5adc --- /dev/null +++ b/assets/controllers/scrapper_import_controller.js @@ -0,0 +1,81 @@ +import { Controller } from '@hotwired/stimulus'; + +/* +* The following line makes this controller "lazy": it won't be downloaded until needed +* See https://github.com/symfony/stimulus-bridge#lazy-controllers +*/ +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + // ... + static targets = ["textarea", "submitButton"] + + connect() { + document.addEventListener('openImportModal', this.prepareImportModal.bind(this)); + document.addEventListener('openExportModal', this.prepareExportModal.bind(this)); + } + + disconnect() { + document.removeEventListener('openImportModal', this.prepareImportModal.bind(this)); + document.removeEventListener('openExportModal', this.prepareExportModal.bind(this)); + } + + async prepareExportModal() { + try { + const response = await fetch('/settings/export_scrappers'); + const data = await response.json(); + this.textareaTarget.value = JSON.stringify(data, null, 2); + this.submitButtonTarget.textContent = 'Copy to Clipboard'; + this.submitButtonTarget.dataset.action = 'scrapper-import#copyToClipboard'; + this.openModal('Export Scrapper Configurations'); + } catch (error) { + console.error('Error:', error); + } + } + + prepareImportModal() { + this.textareaTarget.value = ''; + this.submitButtonTarget.textContent = 'Import'; + this.submitButtonTarget.dataset.action = 'scrapper-import#submitImport'; + this.openModal('Import Scrapper Configurations'); + } + + openModal(title) { + const event = new CustomEvent('openScrapperModal', { detail: { title: title } }); + document.dispatchEvent(event); + } + + async submitImport() { + const jsonData = this.textareaTarget.value; + + try { + const response = await fetch('/settings/import_scrappers', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: jsonData + }); + + const result = await response.json(); + + if (response.ok) { + console.log(result.message); + document.dispatchEvent(new CustomEvent('closeScrapperModal')); + window.location.reload(); + } else { + console.error(result.error); + } + } catch (error) { + console.error('Error:', error); + } + } + + copyToClipboard() { + navigator.clipboard.writeText(this.textareaTarget.value).then(() => { + console.log('Copied to clipboard'); + document.dispatchEvent(new CustomEvent('closeScrapperModal')); + }, (err) => { + console.error('Could not copy text: ', err); + }); + } +} diff --git a/assets/controllers/toolbar_controller.js b/assets/controllers/toolbar_controller.js index 64ed385..dc068e8 100644 --- a/assets/controllers/toolbar_controller.js +++ b/assets/controllers/toolbar_controller.js @@ -99,6 +99,16 @@ export default class extends Controller { document.dispatchEvent(event); } + openImportModal() { + const importEvent = new CustomEvent('openImportModal'); + document.dispatchEvent(importEvent); + } + + openExportModal() { + const exportEvent = new CustomEvent('openExportModal'); + document.dispatchEvent(exportEvent); + } + deleteMangas() { console.log("Deleting mangas..."); } @@ -145,6 +155,7 @@ export default class extends Controller { expandAll() { console.log("Expanding all..."); } + changeView(event) { event.preventDefault(); const viewOption = event.currentTarget.dataset.view; diff --git a/compose.yaml b/compose.yaml index a5c6403..f5e815a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -20,6 +20,8 @@ services: volumes: - caddy_data:/data - caddy_config:/config + - ${MANGA_DATA_PATH:-~/Mangas}:/manga_data + - ${IMAGE_DATA_PATH:-~/MangaImages}:/image_data ports: # HTTP - target: 80 diff --git a/migrations/Version20240603161848.php b/migrations/Version20240603161848.php index f36bc44..fef8693 100644 --- a/migrations/Version20240603161848.php +++ b/migrations/Version20240603161848.php @@ -23,17 +23,14 @@ final class Version20240603161848 extends AbstractMigration $this->addSql('CREATE SEQUENCE api_token_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); $this->addSql('CREATE SEQUENCE chapter_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); $this->addSql('CREATE SEQUENCE manga_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE page_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); $this->addSql('CREATE SEQUENCE source_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); $this->addSql('CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1'); $this->addSql('CREATE TABLE api_token (id INT NOT NULL, owned_by_id INT NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, token VARCHAR(68) NOT NULL, scopes JSON NOT NULL, PRIMARY KEY(id))'); $this->addSql('CREATE INDEX IDX_7BA2F5EB5E70BCD7 ON api_token (owned_by_id)'); $this->addSql('COMMENT ON COLUMN api_token.expires_at IS \'(DC2Type:datetime_immutable)\''); - $this->addSql('CREATE TABLE chapter (id INT NOT NULL, manga_id INT NOT NULL, number DOUBLE PRECISION NOT NULL, pages JSON NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE chapter (id INT NOT NULL, manga_id INT NOT NULL, number DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); $this->addSql('CREATE INDEX IDX_F981B52E7B6461 ON chapter (manga_id)'); $this->addSql('CREATE TABLE manga (id INT NOT NULL, title VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE TABLE page (id INT NOT NULL, chapter_id INT NOT NULL, number INT NOT NULL, image_url VARCHAR(255) NOT NULL, image_local_url VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_140AB620579F4768 ON page (chapter_id)'); $this->addSql('CREATE TABLE source (id INT NOT NULL, name VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, base_url VARCHAR(255) NOT NULL, scrapping_parameters JSON DEFAULT NULL, is_active BOOLEAN NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); $this->addSql('COMMENT ON COLUMN source.created_at IS \'(DC2Type:datetime_immutable)\''); $this->addSql('COMMENT ON COLUMN source.updated_at IS \'(DC2Type:datetime_immutable)\''); @@ -41,7 +38,6 @@ final class Version20240603161848 extends AbstractMigration $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649E7927C74 ON "user" (email)'); $this->addSql('ALTER TABLE api_token ADD CONSTRAINT FK_7BA2F5EB5E70BCD7 FOREIGN KEY (owned_by_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); $this->addSql('ALTER TABLE chapter ADD CONSTRAINT FK_F981B52E7B6461 FOREIGN KEY (manga_id) REFERENCES manga (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE page ADD CONSTRAINT FK_140AB620579F4768 FOREIGN KEY (chapter_id) REFERENCES chapter (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); } public function down(Schema $schema): void @@ -51,16 +47,13 @@ final class Version20240603161848 extends AbstractMigration $this->addSql('DROP SEQUENCE api_token_id_seq CASCADE'); $this->addSql('DROP SEQUENCE chapter_id_seq CASCADE'); $this->addSql('DROP SEQUENCE manga_id_seq CASCADE'); - $this->addSql('DROP SEQUENCE page_id_seq CASCADE'); $this->addSql('DROP SEQUENCE source_id_seq CASCADE'); $this->addSql('DROP SEQUENCE "user_id_seq" CASCADE'); $this->addSql('ALTER TABLE api_token DROP CONSTRAINT FK_7BA2F5EB5E70BCD7'); $this->addSql('ALTER TABLE chapter DROP CONSTRAINT FK_F981B52E7B6461'); - $this->addSql('ALTER TABLE page DROP CONSTRAINT FK_140AB620579F4768'); $this->addSql('DROP TABLE api_token'); $this->addSql('DROP TABLE chapter'); $this->addSql('DROP TABLE manga'); - $this->addSql('DROP TABLE page'); $this->addSql('DROP TABLE source'); $this->addSql('DROP TABLE "user"'); } diff --git a/migrations/Version20240724164344.php b/migrations/Version20240724164344.php new file mode 100644 index 0000000..14ec071 --- /dev/null +++ b/migrations/Version20240724164344.php @@ -0,0 +1,34 @@ +addSql('CREATE SEQUENCE app_settings_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE app_settings (id INT NOT NULL, manga_directory VARCHAR(255) DEFAULT NULL, image_directory VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE app_settings_id_seq CASCADE'); + $this->addSql('DROP TABLE app_settings'); + } +} diff --git a/src/Controller/MangaController.php b/src/Controller/MangaController.php index 83382e0..7a7614a 100644 --- a/src/Controller/MangaController.php +++ b/src/Controller/MangaController.php @@ -222,6 +222,9 @@ class MangaController extends AbstractController } + /** + * @throws GuzzleException + */ #[Route('/addManga', name: 'app_manga_add')] public function addManga(Request $request): Response { @@ -248,8 +251,8 @@ class MangaController extends AbstractController $imageUrls = $this->processAndSaveImage($imageUrl); $manga->setImageUrl($imageUrls['full']); $manga->setThumbnailUrl($imageUrls['thumbnail']); - } catch (\Exception $e) { - // Gérer l'exception (par exemple, logger l'erreur) + } catch (\Exception|GuzzleException $e) { + throw $e; } $mergedChapters = $this->mangadexProvider->addAllChaptersToManga($manga); diff --git a/src/Controller/SettingsController.php b/src/Controller/SettingsController.php index 91337f2..4928a5b 100644 --- a/src/Controller/SettingsController.php +++ b/src/Controller/SettingsController.php @@ -3,7 +3,10 @@ namespace App\Controller; use App\Entity\ContentSource; +use App\Form\AppSettingsType; use App\Form\ContentSourceType; +use App\Manager\AppSettingsManager; +use App\Manager\Toolbar\Factory\ToolbarFactory; use App\Repository\ContentSourceRepository; use App\Service\NotificationService; @@ -21,7 +24,8 @@ class SettingsController extends AbstractController public function __construct( private MangaScraperService $mangaScraperService, private EntityManagerInterface $entityManager, - private NotificationService $notificationService + private NotificationService $notificationService, + private ContentSourceRepository $contentSourceRepository ) { @@ -44,20 +48,34 @@ class SettingsController extends AbstractController } #[Route('/settings/folders', name: 'app_settings_folders')] - public function folders(): Response + public function folders(Request $request, AppSettingsManager $settingsManager): Response { - return $this->render('settings/index.html.twig', [ - 'controller_name' => 'SettingsController', + $currentSettings = $settingsManager->getSettings(); + + $form = $this->createForm(AppSettingsType::class, $currentSettings); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $newSettings = $form->getData(); + $settingsManager->updateSettings($newSettings); + + $this->notificationService->sendUpdate(['status' => 'success', 'message' => 'Settings updated successfully.']); + return $this->json(['success' => true]); + } + + return $this->render('settings/folders.html.twig', [ + 'form' => $form->createView(), ]); } #[Route('/settings/scrappers/list', name: 'app_settings_scrappers_list')] - public function list(ContentSourceRepository $repository): Response + public function list(ContentSourceRepository $repository, ToolbarFactory $toolbarFactory): Response { $contentSources = $repository->findAll(); return $this->render('settings/scrapper_list.html.twig', [ 'contentSources' => $contentSources, + 'toolbar' => $toolbarFactory->createToolbar('scraper_list')->getGroups(), ]); } @@ -137,4 +155,51 @@ class SettingsController extends AbstractController 'controller_name' => 'SettingsController', ]); } + + #[Route('/settings/export_scrappers', name: 'app_settings_scrappers_export', methods: ['GET'])] + public function exportScrappers(): JsonResponse + { + $contentSources = $this->contentSourceRepository->findAll(); + $data = []; + + foreach ($contentSources as $source) { + $data[] = [ + 'baseUrl' => $source->getBaseUrl(), + 'imageSelector' => $source->getImageSelector(), + 'nextPageSelector' => $source->getNextPageSelector(), + 'chapterUrlFormat' => $source->getChapterUrlFormat(), + 'scrapingType' => $source->getScrapingType(), + 'chapterSelector' => $source->getChapterSelector(), //TODO à renommer en chapterListSelector + ]; + } + + return new JsonResponse($data); + } + + #[Route('/settings/import_scrappers', name: 'app_settings_scrappers_import', methods: ['POST'])] + public function importScrappers(Request $request): JsonResponse + { + $content = $request->getContent(); + $data = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Invalid JSON data']); + return new JsonResponse(['error' => 'Invalid JSON data'], 400); + } + + foreach ($data as $sourceData) { + $contentSource = new ContentSource(); + $contentSource->setBaseUrl($sourceData['baseUrl']); + $contentSource->setImageSelector($sourceData['imageSelector']); + $contentSource->setNextPageSelector($sourceData['nextPageSelector']); + $contentSource->setChapterUrlFormat($sourceData['chapterUrlFormat']); + $contentSource->setScrapingType($sourceData['scrapingType']); + + $this->entityManager->persist($contentSource); + } + + $this->entityManager->flush(); + + return new JsonResponse(['message' => 'Content sources imported successfully']); + } } diff --git a/src/Entity/AppSettings.php b/src/Entity/AppSettings.php new file mode 100644 index 0000000..01cffa3 --- /dev/null +++ b/src/Entity/AppSettings.php @@ -0,0 +1,50 @@ +id; + } + + public function getMangaDirectory(): ?string + { + return $this->MangaDirectory; + } + + public function setMangaDirectory(?string $MangaDirectory): static + { + $this->MangaDirectory = $MangaDirectory; + + return $this; + } + + public function getImageDirectory(): ?string + { + return $this->ImageDirectory; + } + + public function setImageDirectory(?string $ImageDirectory): static + { + $this->ImageDirectory = $ImageDirectory; + + return $this; + } +} diff --git a/src/Entity/Chapter.php b/src/Entity/Chapter.php index 865136e..9ea05c5 100644 --- a/src/Entity/Chapter.php +++ b/src/Entity/Chapter.php @@ -20,16 +20,10 @@ class Chapter #[ORM\Column] private ?float $number = null; - #[ORM\Column] - private array $pages = []; - #[ORM\ManyToOne(inversedBy: 'chapters')] #[ORM\JoinColumn(nullable: false)] private ?Manga $manga = null; - #[ORM\OneToMany(mappedBy: 'chapter', targetEntity: Page::class, orphanRemoval: true)] - private Collection $pagesLink; - #[ORM\Column(nullable: true)] private ?int $volume = null; @@ -45,12 +39,11 @@ class Chapter #[ORM\Column(length: 255, nullable: true)] private ?string $cbzPath = null; - #[ORM\Column] + #[ORM\Column(type: 'boolean', options: ['default' => true])] private ?bool $visible = true; public function __construct() { - $this->pagesLink = new ArrayCollection(); } public function getId(): ?int @@ -70,18 +63,6 @@ class Chapter return $this; } - public function getPages(): array - { - return $this->pages; - } - - public function setPages(array $pages): self - { - $this->pages = $pages; - - return $this; - } - public function getManga(): ?Manga { return $this->manga; @@ -94,50 +75,6 @@ class Chapter return $this; } - /** - * @return Collection - */ - public function getPagesLink(): Collection - { - return $this->pagesLink; - } - - public function addPagesLink(Page $pagesLink): self - { - if (!$this->pagesLink->contains($pagesLink)) { - $this->pagesLink->add($pagesLink); - $pagesLink->setChapter($this); - } - - return $this; - } - - public function removePagesLink(Page $pagesLink): self - { - if ($this->pagesLink->removeElement($pagesLink)) { - // set the owning side to null (unless already changed) - if ($pagesLink->getChapter() === $this) { - $pagesLink->setChapter(null); - } - } - - return $this; - } - - public function getPageByNumber(int $number): ?Page - { - /** - * @var Page $page - */ - foreach ($this->pagesLink as $page) { - if ($page->getNumber() === $number) { - return $page; - } - } - - return null; - } - public function getVolume(): ?int { return $this->volume; diff --git a/src/Entity/Page.php b/src/Entity/Page.php deleted file mode 100644 index 0183439..0000000 --- a/src/Entity/Page.php +++ /dev/null @@ -1,81 +0,0 @@ -id; - } - - public function getNumber(): ?int - { - return $this->number; - } - - public function setNumber(int $number): self - { - $this->number = $number; - - return $this; - } - - public function getImageUrl(): ?string - { - return $this->imageUrl; - } - - public function setImageUrl(string $imageUrl): self - { - $this->imageUrl = $imageUrl; - - return $this; - } - - public function getChapter(): ?Chapter - { - return $this->chapter; - } - - public function setChapter(?Chapter $chapter): self - { - $this->chapter = $chapter; - - return $this; - } - - public function getImageLocalUrl(): ?string - { - return $this->imageLocalUrl; - } - - public function setImageLocalUrl(string $imageLocalUrl): self - { - $this->imageLocalUrl = $imageLocalUrl; - - return $this; - } -} diff --git a/src/Form/AppSettingsType.php b/src/Form/AppSettingsType.php new file mode 100644 index 0000000..d6e7371 --- /dev/null +++ b/src/Form/AppSettingsType.php @@ -0,0 +1,30 @@ +add('mangaDirectory', TextType::class, [ + 'label' => 'Manga Directory', + ]) + ->add('imageDirectory', TextType::class, [ + 'label' => 'Image Directory', + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => AppSettings::class, + ]); + } +} diff --git a/src/Form/ContentSourceType.php b/src/Form/ContentSourceType.php index a693d1d..9fa3b28 100644 --- a/src/Form/ContentSourceType.php +++ b/src/Form/ContentSourceType.php @@ -22,7 +22,7 @@ class ContentSourceType extends AbstractType 'label' => 'Image Selector', ]) ->add('chapterUrlFormat', TextType::class, [ - 'label' => 'Chapter URL Format', + 'label' => 'Chapter URL Format ({slug}, {chapterNumber})', ]) ->add('nextPageSelector', TextType::class, [ 'label' => 'Next Page Selector (let empty if vertical reader)', diff --git a/src/Manager/AppSettingsManager.php b/src/Manager/AppSettingsManager.php new file mode 100644 index 0000000..bccedd9 --- /dev/null +++ b/src/Manager/AppSettingsManager.php @@ -0,0 +1,52 @@ +entityManager->getRepository(AppSettings::class)->findOneBy([]); + if (!$settings) { + $settings = $this->createDefaultSettings(); + } + + return $settings; + } + + public function updateSettings(AppSettings $newSettings): void + { + $settings = $this->entityManager->getRepository(AppSettings::class)->findOneBy([]); + if (!$settings) { + $settings = new AppSettings(); + } + + $settings->setMangaDirectory($newSettings->getMangaDirectory()); + $settings->setImageDirectory($newSettings->getImageDirectory()); + + $this->entityManager->persist($settings); + $this->entityManager->flush(); + } + + private function createDefaultSettings(): AppSettings + { + $settings = new AppSettings(); + $settings->setMangaDirectory(self::DEFAULT_MANGA_DIRECTORY); + $settings->setImageDirectory(self::DEFAULT_IMAGE_DIRECTORY); + + $this->entityManager->persist($settings); + $this->entityManager->flush(); + + return $settings; + } +} diff --git a/src/Manager/FileSystemManager.php b/src/Manager/FileSystemManager.php index 4cf757e..39042b4 100644 --- a/src/Manager/FileSystemManager.php +++ b/src/Manager/FileSystemManager.php @@ -11,17 +11,48 @@ class FileSystemManager private const string UPLOADS_DIRECTORY = 'public/tmp'; private const string IMAGES_DIRECTORY = 'public/images'; + private string $mangaDirectory; + private string $imageDirectory; + public function __construct( - private readonly string $projectDir, - private readonly Filesystem $filesystem, - private readonly SluggerInterface $slugger - ) { + private readonly string $projectDir, + private readonly Filesystem $filesystem, + private readonly SluggerInterface $slugger, + private readonly AppSettingsManager $appSettingsManager + ) + { + $this->loadSettings(); + } + + private function loadSettings(): void + { + $settings = $this->appSettingsManager->getSettings(); + $this->mangaDirectory = $settings->getMangaDirectory(); + $this->imageDirectory = $settings->getImageDirectory(); + } + + public function getMangaDirectory(): string + { + return $this->mangaDirectory; + } + + public function getImageDirectory(): string + { + return $this->imageDirectory; + } + + public function getImagePath(string $subDir = ''): string + { + if(!$this->filesystem->exists($this->projectDir. '/' . self::IMAGES_DIRECTORY . ($subDir ? "/$subDir" : ''))) { + $this->filesystem->mkdir($this->projectDir. '/' . self::IMAGES_DIRECTORY . ($subDir ? "/$subDir" : ''), 0755); + } + return $this->projectDir. '/' . self::IMAGES_DIRECTORY . ($subDir ? "/$subDir" : ''); } public function createMangaDirectory(string $mangaSlug, ?int $year): string { $year = $year ?? 'unknown'; - $directoryPath = $this->projectDir . '/' . self::CBZ_DIRECTORY . '/' . ucfirst($mangaSlug) . " ($year)"; + $directoryPath = $this->mangaDirectory . '/' . ucfirst($mangaSlug) . " ($year)"; $this->filesystem->mkdir($directoryPath, 0755); return $directoryPath; } @@ -76,11 +107,6 @@ class FileSystemManager return $safeFilename . '-' . uniqid() . '.' . pathinfo($originalFilename, PATHINFO_EXTENSION); } - public function getImagePath(string $subDir = ''): string - { - return $this->projectDir . '/' . self::IMAGES_DIRECTORY . ($subDir ? "/$subDir" : ''); - } - public function generateUniqueImageFilename(string $originalFilename): string { $safeFilename = $this->slugger->slug(pathinfo($originalFilename, PATHINFO_FILENAME)); diff --git a/src/Manager/Toolbar/Definition/ScraperListToolbar.php b/src/Manager/Toolbar/Definition/ScraperListToolbar.php new file mode 100644 index 0000000..a27b2ce --- /dev/null +++ b/src/Manager/Toolbar/Definition/ScraperListToolbar.php @@ -0,0 +1,16 @@ +addToRightGroup(new ToolbarButton('file-import', 'Import Json', 'toolbar#openImportModal')) + ->addToRightGroup(new ToolbarDivider()) + ->addToRightGroup(new ToolbarButton('file-export', 'Export Json', 'toolbar#openExportModal')); + } +} diff --git a/src/Manager/Toolbar/Factory/ToolbarFactory.php b/src/Manager/Toolbar/Factory/ToolbarFactory.php index cfbab3a..2c35e08 100644 --- a/src/Manager/Toolbar/Factory/ToolbarFactory.php +++ b/src/Manager/Toolbar/Factory/ToolbarFactory.php @@ -5,6 +5,7 @@ namespace App\Manager\Toolbar\Factory; use App\Manager\Toolbar\Definition\ActivityToolbar; use App\Manager\Toolbar\Definition\ChapterListToolbar; use App\Manager\Toolbar\Definition\MangaListToolbar; +use App\Manager\Toolbar\Definition\ScraperListToolbar; use App\Manager\Toolbar\Definition\Toolbar; class ToolbarFactory @@ -15,6 +16,7 @@ class ToolbarFactory 'manga_list' => new MangaListToolbar(), 'chapter_list' => new ChapterListToolbar($context), 'activity' => new ActivityToolbar($context), + 'scraper_list' => new ScraperListToolbar($context), default => throw new \InvalidArgumentException("Unknown toolbar type: $type"), }; } diff --git a/src/Repository/AppSettingsRepository.php b/src/Repository/AppSettingsRepository.php new file mode 100644 index 0000000..196b8e4 --- /dev/null +++ b/src/Repository/AppSettingsRepository.php @@ -0,0 +1,48 @@ + + * + * @method AppSettings|null find($id, $lockMode = null, $lockVersion = null) + * @method AppSettings|null findOneBy(array $criteria, array $orderBy = null) + * @method AppSettings[] findAll() + * @method AppSettings[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class AppSettingsRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, AppSettings::class); + } + +// /** +// * @return AppSettings[] Returns an array of AppSettings objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('a') +// ->andWhere('a.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('a.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?AppSettings +// { +// return $this->createQueryBuilder('a') +// ->andWhere('a.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/templates/components/Modal.html.twig b/templates/components/Modal.html.twig index 17e1965..e228f28 100644 --- a/templates/components/Modal.html.twig +++ b/templates/components/Modal.html.twig @@ -15,7 +15,7 @@ {# Modal panel #} -
+