diff --git a/assets/controllers/activity_controller.js b/assets/controllers/activity_controller.js index 84abf4e..981a7ea 100644 --- a/assets/controllers/activity_controller.js +++ b/assets/controllers/activity_controller.js @@ -35,7 +35,6 @@ export default class extends Controller { eventSource.onmessage = (event) => { const data = JSON.parse(event.data); - console.log(data); if (data.processing !== undefined && data.pending !== undefined) { let totalActivities = data.processing.length + data.pending.length; this.activityTarget.innerHTML = totalActivities; diff --git a/assets/controllers/chapter_progress_controller.js b/assets/controllers/chapter_progress_controller.js index 903749c..f54b149 100644 --- a/assets/controllers/chapter_progress_controller.js +++ b/assets/controllers/chapter_progress_controller.js @@ -10,7 +10,6 @@ export default class extends Controller { connect() { this.currentPage = 0; this.totalPages = 0; - this.progressBarElement = this.progressBarTarget.querySelector('.bg-blue-600'); const mercureHubUrl = 'https://localhost/.well-known/mercure'; this.eventSource = new EventSource(`${mercureHubUrl}?topic=activity`); @@ -26,7 +25,7 @@ export default class extends Controller { handleMessage(event) { const data = JSON.parse(event.data); - if (data.status === "Page Scrapping progress" && data.chapterId === this.chapterIdValue) { + if (data.status === "scrapping.progress" && data.chapterId === this.chapterIdValue) { this.handleProgressUpdate(data); } } @@ -35,16 +34,12 @@ export default class extends Controller { this.currentPage = data.pageIndex + 1; this.totalPages = data.totalPages; - if (this.currentPage > 1) { - this.progressBarTarget.classList.remove('hidden'); - } - this.updateProgressBar(); } updateProgressBar() { const progress = (this.currentPage / this.totalPages) * 100; - this.progressBarElement.style.width = `${progress}%`; + this.progressBarTarget.style.width = `${progress}%`; this.progressTextTarget.textContent = `${this.currentPage} / ${this.totalPages}`; } } diff --git a/assets/controllers/toolbar_controller.js b/assets/controllers/toolbar_controller.js index 5bf71b1..0242c4a 100644 --- a/assets/controllers/toolbar_controller.js +++ b/assets/controllers/toolbar_controller.js @@ -1,20 +1,24 @@ // assets/controllers/toolbar_controller.js import { Controller } from "@hotwired/stimulus" +import { visit } from "@hotwired/turbo" export default class extends Controller { static targets = ["dropdown", "icon"] static values = { currentSort: String, currentOrder: String, - currentStatus: String + currentStatus: String, + mangaId: Number } connect() { window.addEventListener('alert:show', this.stopLoading.bind(this)); } - stopLoading() { - this.iconTarget.classList.remove('fa-spin'); + stopLoading(event) { + if(event.currentTarget.dataset !== undefined){ + this.iconTarget.classList.remove('fa-spin'); + } } refreshMetadata(event) { @@ -65,6 +69,36 @@ export default class extends Controller { document.dispatchEvent(event); } + confirmDelete(event) { + event.preventDefault(); + const url = `/manga/delete/${this.mangaIdValue}`; + + fetch(url, { + method: 'DELETE', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/json', + } + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.success) { + visit('/', {}); + } else { + throw new Error(data.error); + } + }) + .catch(error => { + console.error('Error:', error); + // Show error message to user + }); + } + showOptions() { console.log("Showing options..."); } diff --git a/src/Controller/ActivityController.php b/src/Controller/ActivityController.php index 9a6fcae..3f4a199 100644 --- a/src/Controller/ActivityController.php +++ b/src/Controller/ActivityController.php @@ -2,6 +2,7 @@ namespace App\Controller; +use App\Manager\Toolbar\Factory\ToolbarFactory; use App\Manager\ToolbarManager; use App\Message\DownloadChapter; use App\Repository\ChapterRepository; @@ -17,7 +18,7 @@ class ActivityController extends AbstractController public function __construct( private readonly Connection $connection, private readonly ChapterRepository $chapterRepository, - private readonly ToolbarManager $toolbarManager + private readonly ToolbarFactory $toolbarFactory ) { @@ -38,7 +39,7 @@ class ActivityController extends AbstractController return $this->render('activity/index.html.twig', [ 'controller_name' => 'ActivityController', 'status' => $status, - 'toolbarItems' => $this->toolbarManager->getToolbarItems(), + 'toolbar' => $this->toolbarFactory->createToolbar('activity')->getGroups(), ]); } diff --git a/src/Controller/ImportController.php b/src/Controller/ImportController.php index 55219df..85f7b3a 100644 --- a/src/Controller/ImportController.php +++ b/src/Controller/ImportController.php @@ -49,13 +49,13 @@ class ImportController extends AbstractController return $this->redirectToRoute('import_match'); } catch (FileException $e) { $this->notificationService->sendUpdate([ - 'type' => 'error', + 'status' => 'error', 'message' => 'Une erreur est survenue lors de l\'import du fichier.' ]); } } else { $this->notificationService->sendUpdate([ - 'type' => 'error', + 'status' => 'error', 'message' => 'Le fichier doit être au format CBZ.' ]); } @@ -79,7 +79,7 @@ class ImportController extends AbstractController $metadata = $this->cbzService->extractMetadata($filePath, $originalFileName); if($metadata['title'] === '' || is_null($metadata['title'])){ $this->notificationService->sendUpdate([ - 'type' => 'error', + 'status' => 'error', 'message' => 'Impossible de détecter le titre du manga.' ]); return $this->redirectToRoute('app_manga_import'); @@ -108,10 +108,10 @@ class ImportController extends AbstractController if(empty($mangas)) { $this->notificationService->sendUpdate([ - 'type' => 'error', + 'status' => 'error', 'message' => 'Aucun manga trouvé avec ce titre.' ]); - return $this->redirectToRoute('app_manga_new', ['query' => $metadata['title']]); + return $this->redirectToRoute('app_manga_search', ['query' => $metadata['title']]); } if ($request->isMethod('post')) { @@ -142,7 +142,7 @@ class ImportController extends AbstractController $manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]); if (!$manga) { $this->notificationService->sendUpdate([ - 'type' => 'error', + 'status' => 'error', 'message' => 'Manga non trouvé.' ]); return $this->redirectToRoute('app_manga_import'); @@ -151,7 +151,7 @@ class ImportController extends AbstractController $filePath = $session->get('import_file_path'); if (!$filePath) { $this->notificationService->sendUpdate([ - 'type' => 'error', + 'status' => 'error', 'message' => 'Fichier d\'import non trouvé.' ]); return $this->redirectToRoute('app_manga_import'); @@ -166,13 +166,13 @@ class ImportController extends AbstractController $this->mangaImportService->importVolume($manga, (int)$volume, $filePath, $originalFileName); } catch (\Exception $e) { $this->notificationService->sendUpdate([ - 'type' => 'error', + 'status' => 'error', 'message' => 'Erreur lors de l\'import : ' . $e->getMessage() ]); } $this->notificationService->sendUpdate([ - 'type' => 'success', + 'status' => 'success', 'message' => 'Import confirmé avec succès.' ]); @@ -187,7 +187,7 @@ class ImportController extends AbstractController $session->remove('import_original_file_name'); $this->notificationService->sendUpdate([ - 'type' => 'info', + 'status' => 'info', 'message' => 'Import refusé. Le fichier a été supprimé.' ]); } diff --git a/src/Controller/MangaController.php b/src/Controller/MangaController.php index 410b18e..e76446a 100644 --- a/src/Controller/MangaController.php +++ b/src/Controller/MangaController.php @@ -84,6 +84,22 @@ class MangaController extends AbstractController ]); } + #[Route('/manga/delete/{id}', name: 'app_manga_delete', methods: ['DELETE'])] + public function deleteManga(Manga $manga): JsonResponse + { + try { + foreach ($manga->getChapters() as $chapter) { + $this->entityManager->remove($chapter); + } + $this->entityManager->remove($manga); + $this->entityManager->flush(); + + return new JsonResponse(['success' => true]); + } catch (\Exception $e) { + return new JsonResponse(['success' => false, 'error' => 'Unable to delete manga.'], 500); + } + } + public function _chaptersByManga(int $id): Response { @@ -291,7 +307,7 @@ class MangaController extends AbstractController ]); if (empty($volumeChapters)) { - $this->notificationService->sendUpdate(['error' => 'No chapters found for this volume.']); + $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'No chapters found for this volume.']); return new JsonResponse(['error' => 'No chapters found for this volume.'], 200); } @@ -309,13 +325,13 @@ class MangaController extends AbstractController { $chapter = $this->chapterRepository->find($chapterId); if (!$chapter) { - $this->notificationService->sendUpdate(['error' => 'Chapitre non trouvé.']); + $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Chapitre non trouvé.']); return new JsonResponse(['error' => 'Chapitre non trouvé.'], 200); } $cbzPath = $chapter->getCbzPath(); if (!$cbzPath || !file_exists($cbzPath)) { - $this->notificationService->sendUpdate(['error' => 'Le fichier CBZ n\'existe pas.']); + $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Le fichier CBZ n\'existe pas.']); return new JsonResponse(['error' => 'Le fichier CBZ n\'existe pas.'], 200); } @@ -338,11 +354,11 @@ class MangaController extends AbstractController ]); if (empty($volumeChapters)) { - $this->notificationService->sendUpdate(['error' => 'Aucun chapitre trouvé pour ce volume.']); + $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Aucun chapitre trouvé pour ce volume.']); } if (!$this->cbzService->doAllChaptersHaveCbz($volumeChapters)) { - $this->notificationService->sendUpdate(['error' => 'Tous les chapitres du volume ne sont pas scrapés.']); + $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Tous les chapitres du volume ne sont pas scrapés.']); return new JsonResponse(['error' => 'Tous les chapitres du volume ne sont pas scrapés.'], 200); } diff --git a/src/Controller/SettingsController.php b/src/Controller/SettingsController.php index 8c4ec55..e692f91 100644 --- a/src/Controller/SettingsController.php +++ b/src/Controller/SettingsController.php @@ -6,6 +6,7 @@ use App\Entity\ContentSource; use App\Form\ContentSourceType; use App\Repository\ContentSourceRepository; use App\Service\MangaScraperService; +use App\Service\NotificationService; use Doctrine\ORM\EntityManagerInterface; use GuzzleHttp\Exception\GuzzleException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -18,7 +19,8 @@ class SettingsController extends AbstractController { public function __construct( private MangaScraperService $mangaScraperService, - private EntityManagerInterface $entityManager + private EntityManagerInterface $entityManager, + private NotificationService $notificationService ) { @@ -70,7 +72,7 @@ class SettingsController extends AbstractController if ($form->isSubmitted() && $form->isValid()) { $this->entityManager->persist($contentSource); $this->entityManager->flush(); - $this->addFlash('success', ($isNew ? 'New scrapper configuration saved' : 'Scrapper configuration updated') . ' successfully.'); + $this->notificationService->sendUpdate(['status' => 'success', 'message' => ($isNew ? 'New scrapper configuration saved' : 'Scrapper configuration updated') . ' successfully.']); return $this->redirectToRoute('app_settings_scrappers_list'); } @@ -94,7 +96,15 @@ class SettingsController extends AbstractController $mangaSlug = $request->request->get('mangaSlug'); $chapterNumber = $request->request->get('chapterNumber'); - $scrapedData = $this->mangaScraperService->testScrapingHtml($mangaSlug, $chapterNumber, $contentSource); + try { + $scrapedData = $this->mangaScraperService->testScrapingHtml($mangaSlug, $chapterNumber, $contentSource); + }catch (\Exception $e){ + $this->notificationService->sendUpdate(['status' => 'error', 'message' => $e->getMessage()]); + return new JsonResponse([ + 'success' => false, + 'message' => $e->getMessage(), + ]); + } return new JsonResponse([ 'success' => true, diff --git a/src/Entity/ContentSource.php b/src/Entity/ContentSource.php index cc05e8c..cc1ee8c 100644 --- a/src/Entity/ContentSource.php +++ b/src/Entity/ContentSource.php @@ -3,11 +3,16 @@ namespace App\Entity; use App\Repository\ContentSourceRepository; +use App\Service\ChapterUrlGenerator; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: ContentSourceRepository::class)] class ContentSource { + public function __construct() + { + } + #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] @@ -83,7 +88,8 @@ class ContentSource public function getChapterUrl(string $mangaTitle, float $chapterNumber): string { - return sprintf($this->chapterUrlFormat, $mangaTitle, $chapterNumber); + $urlGenerator = new ChapterUrlGenerator($this->chapterUrlFormat); + return $urlGenerator->getChapterUrl($mangaTitle, $chapterNumber); } public function getScrapingType(): ?string diff --git a/src/EventSubscriber/QueueStatusSubscriber.php b/src/EventSubscriber/QueueStatusSubscriber.php index 9ebaf45..2c304a6 100644 --- a/src/EventSubscriber/QueueStatusSubscriber.php +++ b/src/EventSubscriber/QueueStatusSubscriber.php @@ -66,7 +66,7 @@ class QueueStatusSubscriber implements EventSubscriberInterface public function onPageScrapingProgress(PageScrappingProgressEvent $event): void { $data = [ - 'status' => 'Page scraping progress', + 'status' => 'scrapping.progress', 'chapterId' => $event->getChapterId(), 'pageIndex' => $event->getPageIndex(), 'totalPages' => $event->getTotalPages(), diff --git a/src/Manager/Toolbar/Definition/ActivityToolbar.php b/src/Manager/Toolbar/Definition/ActivityToolbar.php new file mode 100644 index 0000000..3c6b666 --- /dev/null +++ b/src/Manager/Toolbar/Definition/ActivityToolbar.php @@ -0,0 +1,20 @@ +addToLeftGroup(new ToolbarButton('arrows-rotate', 'Refresh', 'toolbar#refreshActivity')) + ->addToLeftGroup(new ToolbarDivider()) + ->addToLeftGroup(new ToolbarButton('trash-can', 'Remove Selected', 'toolbar#removeActivity')) + + ->addToRightGroup(new ToolbarButton('th-large', 'Options', 'toolbar#optionActivity')) + ; + } +} diff --git a/src/Manager/Toolbar/Definition/ChapterListToolbar.php b/src/Manager/Toolbar/Definition/ChapterListToolbar.php index de0ea52..654ad65 100644 --- a/src/Manager/Toolbar/Definition/ChapterListToolbar.php +++ b/src/Manager/Toolbar/Definition/ChapterListToolbar.php @@ -10,17 +10,17 @@ class ChapterListToolbar extends Toolbar public function __construct(array $contextData = []) { $this - ->addToLeftGroup(new ToolbarButton('arrows-rotate', 'Refresh metadata', 'refreshMetadata', $contextData)) + ->addToLeftGroup(new ToolbarButton('arrows-rotate', 'Refresh metadata', 'toolbar#refreshMetadata', $contextData)) ->addToLeftGroup(new ToolbarDivider()) - ->addToLeftGroup(new ToolbarButton('keyboard', 'Rename chapters', 'renameChapters')) - ->addToLeftGroup(new ToolbarButton('file-zipper', 'Manage cbz', 'manageCbz', $contextData)) - ->addToLeftGroup(new ToolbarButton('history', 'History', 'history', $contextData)) + ->addToLeftGroup(new ToolbarButton('keyboard', 'Rename chapters', 'toolbar#renameChapters')) + ->addToLeftGroup(new ToolbarButton('file-zipper', 'Manage cbz', 'toolbar#manageCbz', $contextData)) + ->addToLeftGroup(new ToolbarButton('history', 'History', 'toolbar#history', $contextData)) - ->addToRightGroup(new ToolbarButton('bookmark', 'Monitoring', 'monitoring', $contextData)) - ->addToRightGroup(new ToolbarButton('wrench', 'Edit', 'editManga', $contextData)) - ->addToRightGroup(new ToolbarButton('trash-can', 'Delete', 'deleteManga', $contextData)) + ->addToRightGroup(new ToolbarButton('bookmark', 'Monitoring', 'toolbar#monitoring', $contextData)) + ->addToRightGroup(new ToolbarButton('wrench', 'Edit', 'toolbar#editManga', $contextData)) + ->addToRightGroup(new ToolbarButton('trash-can', 'Delete', 'toolbar#deleteManga', $contextData)) ->addToRightGroup(new ToolbarDivider()) - ->addToRightGroup(new ToolbarButton('chevron-down', 'Expand all', 'expandAll')); + ->addToRightGroup(new ToolbarButton('chevron-down', 'Expand all', 'toolbar#expandAll')); } } diff --git a/src/Manager/Toolbar/Factory/ToolbarFactory.php b/src/Manager/Toolbar/Factory/ToolbarFactory.php index 39fb6a5..cfbab3a 100644 --- a/src/Manager/Toolbar/Factory/ToolbarFactory.php +++ b/src/Manager/Toolbar/Factory/ToolbarFactory.php @@ -2,6 +2,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\Toolbar; @@ -13,6 +14,7 @@ class ToolbarFactory return match ($type) { 'manga_list' => new MangaListToolbar(), 'chapter_list' => new ChapterListToolbar($context), + 'activity' => new ActivityToolbar($context), default => throw new \InvalidArgumentException("Unknown toolbar type: $type"), }; } diff --git a/src/Service/ChapterUrlGenerator.php b/src/Service/ChapterUrlGenerator.php new file mode 100644 index 0000000..0529d14 --- /dev/null +++ b/src/Service/ChapterUrlGenerator.php @@ -0,0 +1,34 @@ +chapterUrlFormat = $chapterUrlFormat; + $this->validateUrlFormat($chapterUrlFormat); + } + + public function getChapterUrl(string $mangaTitle, float $chapterNumber): string + { + $placeholders = [ + '{chapterNumber}' => $chapterNumber, + '{slug}' => $mangaTitle, + ]; + + return str_replace(array_keys($placeholders), array_values($placeholders), $this->chapterUrlFormat); + } + + private function validateUrlFormat(string $format): void + { + if (!str_contains($format, '{slug}') || !str_contains($format, '{chapterNumber}')) { + throw new InvalidArgumentException("The URL format must contain both {slug} and {chapterNumber} placeholders."); + } + } + +} diff --git a/templates/activity/index.html.twig b/templates/activity/index.html.twig index 777c725..0316579 100644 --- a/templates/activity/index.html.twig +++ b/templates/activity/index.html.twig @@ -5,12 +5,12 @@ {% endif %} {% endblock %} {% block body %} -
+
- + @@ -18,14 +18,13 @@ + {% for manga in status %} - + @@ -33,15 +32,21 @@ + + -
Volume Chapitre TitreProgress Actions
{{ manga.volume }} {{ manga.chapter }} - {{ manga.title }} +
+
+
+
+ 0 / 0 +
{{ manga.title }} - +