Added:
- fix progressbar
- {slug} {chapterNumber} in Url
- activity toolbar
This commit is contained in:
@@ -35,7 +35,6 @@ export default class extends Controller {
|
|||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
console.log(data);
|
|
||||||
if (data.processing !== undefined && data.pending !== undefined) {
|
if (data.processing !== undefined && data.pending !== undefined) {
|
||||||
let totalActivities = data.processing.length + data.pending.length;
|
let totalActivities = data.processing.length + data.pending.length;
|
||||||
this.activityTarget.innerHTML = totalActivities;
|
this.activityTarget.innerHTML = totalActivities;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export default class extends Controller {
|
|||||||
connect() {
|
connect() {
|
||||||
this.currentPage = 0;
|
this.currentPage = 0;
|
||||||
this.totalPages = 0;
|
this.totalPages = 0;
|
||||||
this.progressBarElement = this.progressBarTarget.querySelector('.bg-blue-600');
|
|
||||||
|
|
||||||
const mercureHubUrl = 'https://localhost/.well-known/mercure';
|
const mercureHubUrl = 'https://localhost/.well-known/mercure';
|
||||||
this.eventSource = new EventSource(`${mercureHubUrl}?topic=activity`);
|
this.eventSource = new EventSource(`${mercureHubUrl}?topic=activity`);
|
||||||
@@ -26,7 +25,7 @@ export default class extends Controller {
|
|||||||
|
|
||||||
handleMessage(event) {
|
handleMessage(event) {
|
||||||
const data = JSON.parse(event.data);
|
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);
|
this.handleProgressUpdate(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,16 +34,12 @@ export default class extends Controller {
|
|||||||
this.currentPage = data.pageIndex + 1;
|
this.currentPage = data.pageIndex + 1;
|
||||||
this.totalPages = data.totalPages;
|
this.totalPages = data.totalPages;
|
||||||
|
|
||||||
if (this.currentPage > 1) {
|
|
||||||
this.progressBarTarget.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateProgressBar();
|
this.updateProgressBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProgressBar() {
|
updateProgressBar() {
|
||||||
const progress = (this.currentPage / this.totalPages) * 100;
|
const progress = (this.currentPage / this.totalPages) * 100;
|
||||||
this.progressBarElement.style.width = `${progress}%`;
|
this.progressBarTarget.style.width = `${progress}%`;
|
||||||
this.progressTextTarget.textContent = `${this.currentPage} / ${this.totalPages}`;
|
this.progressTextTarget.textContent = `${this.currentPage} / ${this.totalPages}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
// assets/controllers/toolbar_controller.js
|
// assets/controllers/toolbar_controller.js
|
||||||
import { Controller } from "@hotwired/stimulus"
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import { visit } from "@hotwired/turbo"
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ["dropdown", "icon"]
|
static targets = ["dropdown", "icon"]
|
||||||
static values = {
|
static values = {
|
||||||
currentSort: String,
|
currentSort: String,
|
||||||
currentOrder: String,
|
currentOrder: String,
|
||||||
currentStatus: String
|
currentStatus: String,
|
||||||
|
mangaId: Number
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
window.addEventListener('alert:show', this.stopLoading.bind(this));
|
window.addEventListener('alert:show', this.stopLoading.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
stopLoading() {
|
stopLoading(event) {
|
||||||
this.iconTarget.classList.remove('fa-spin');
|
if(event.currentTarget.dataset !== undefined){
|
||||||
|
this.iconTarget.classList.remove('fa-spin');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshMetadata(event) {
|
refreshMetadata(event) {
|
||||||
@@ -65,6 +69,36 @@ export default class extends Controller {
|
|||||||
document.dispatchEvent(event);
|
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() {
|
showOptions() {
|
||||||
console.log("Showing options...");
|
console.log("Showing options...");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Manager\Toolbar\Factory\ToolbarFactory;
|
||||||
use App\Manager\ToolbarManager;
|
use App\Manager\ToolbarManager;
|
||||||
use App\Message\DownloadChapter;
|
use App\Message\DownloadChapter;
|
||||||
use App\Repository\ChapterRepository;
|
use App\Repository\ChapterRepository;
|
||||||
@@ -17,7 +18,7 @@ class ActivityController extends AbstractController
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Connection $connection,
|
private readonly Connection $connection,
|
||||||
private readonly ChapterRepository $chapterRepository,
|
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', [
|
return $this->render('activity/index.html.twig', [
|
||||||
'controller_name' => 'ActivityController',
|
'controller_name' => 'ActivityController',
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'toolbarItems' => $this->toolbarManager->getToolbarItems(),
|
'toolbar' => $this->toolbarFactory->createToolbar('activity')->getGroups(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ class ImportController extends AbstractController
|
|||||||
return $this->redirectToRoute('import_match');
|
return $this->redirectToRoute('import_match');
|
||||||
} catch (FileException $e) {
|
} catch (FileException $e) {
|
||||||
$this->notificationService->sendUpdate([
|
$this->notificationService->sendUpdate([
|
||||||
'type' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Une erreur est survenue lors de l\'import du fichier.'
|
'message' => 'Une erreur est survenue lors de l\'import du fichier.'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->notificationService->sendUpdate([
|
$this->notificationService->sendUpdate([
|
||||||
'type' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Le fichier doit être au format CBZ.'
|
'message' => 'Le fichier doit être au format CBZ.'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ class ImportController extends AbstractController
|
|||||||
$metadata = $this->cbzService->extractMetadata($filePath, $originalFileName);
|
$metadata = $this->cbzService->extractMetadata($filePath, $originalFileName);
|
||||||
if($metadata['title'] === '' || is_null($metadata['title'])){
|
if($metadata['title'] === '' || is_null($metadata['title'])){
|
||||||
$this->notificationService->sendUpdate([
|
$this->notificationService->sendUpdate([
|
||||||
'type' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Impossible de détecter le titre du manga.'
|
'message' => 'Impossible de détecter le titre du manga.'
|
||||||
]);
|
]);
|
||||||
return $this->redirectToRoute('app_manga_import');
|
return $this->redirectToRoute('app_manga_import');
|
||||||
@@ -108,10 +108,10 @@ class ImportController extends AbstractController
|
|||||||
|
|
||||||
if(empty($mangas)) {
|
if(empty($mangas)) {
|
||||||
$this->notificationService->sendUpdate([
|
$this->notificationService->sendUpdate([
|
||||||
'type' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Aucun manga trouvé avec ce titre.'
|
'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')) {
|
if ($request->isMethod('post')) {
|
||||||
@@ -142,7 +142,7 @@ class ImportController extends AbstractController
|
|||||||
$manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]);
|
$manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]);
|
||||||
if (!$manga) {
|
if (!$manga) {
|
||||||
$this->notificationService->sendUpdate([
|
$this->notificationService->sendUpdate([
|
||||||
'type' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Manga non trouvé.'
|
'message' => 'Manga non trouvé.'
|
||||||
]);
|
]);
|
||||||
return $this->redirectToRoute('app_manga_import');
|
return $this->redirectToRoute('app_manga_import');
|
||||||
@@ -151,7 +151,7 @@ class ImportController extends AbstractController
|
|||||||
$filePath = $session->get('import_file_path');
|
$filePath = $session->get('import_file_path');
|
||||||
if (!$filePath) {
|
if (!$filePath) {
|
||||||
$this->notificationService->sendUpdate([
|
$this->notificationService->sendUpdate([
|
||||||
'type' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Fichier d\'import non trouvé.'
|
'message' => 'Fichier d\'import non trouvé.'
|
||||||
]);
|
]);
|
||||||
return $this->redirectToRoute('app_manga_import');
|
return $this->redirectToRoute('app_manga_import');
|
||||||
@@ -166,13 +166,13 @@ class ImportController extends AbstractController
|
|||||||
$this->mangaImportService->importVolume($manga, (int)$volume, $filePath, $originalFileName);
|
$this->mangaImportService->importVolume($manga, (int)$volume, $filePath, $originalFileName);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->notificationService->sendUpdate([
|
$this->notificationService->sendUpdate([
|
||||||
'type' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Erreur lors de l\'import : ' . $e->getMessage()
|
'message' => 'Erreur lors de l\'import : ' . $e->getMessage()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->notificationService->sendUpdate([
|
$this->notificationService->sendUpdate([
|
||||||
'type' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Import confirmé avec succès.'
|
'message' => 'Import confirmé avec succès.'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ class ImportController extends AbstractController
|
|||||||
$session->remove('import_original_file_name');
|
$session->remove('import_original_file_name');
|
||||||
|
|
||||||
$this->notificationService->sendUpdate([
|
$this->notificationService->sendUpdate([
|
||||||
'type' => 'info',
|
'status' => 'info',
|
||||||
'message' => 'Import refusé. Le fichier a été supprimé.'
|
'message' => 'Import refusé. Le fichier a été supprimé.'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
public function _chaptersByManga(int $id): Response
|
||||||
{
|
{
|
||||||
@@ -291,7 +307,7 @@ class MangaController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (empty($volumeChapters)) {
|
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);
|
return new JsonResponse(['error' => 'No chapters found for this volume.'], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,13 +325,13 @@ class MangaController extends AbstractController
|
|||||||
{
|
{
|
||||||
$chapter = $this->chapterRepository->find($chapterId);
|
$chapter = $this->chapterRepository->find($chapterId);
|
||||||
if (!$chapter) {
|
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);
|
return new JsonResponse(['error' => 'Chapitre non trouvé.'], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
$cbzPath = $chapter->getCbzPath();
|
$cbzPath = $chapter->getCbzPath();
|
||||||
if (!$cbzPath || !file_exists($cbzPath)) {
|
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);
|
return new JsonResponse(['error' => 'Le fichier CBZ n\'existe pas.'], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,11 +354,11 @@ class MangaController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (empty($volumeChapters)) {
|
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)) {
|
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);
|
return new JsonResponse(['error' => 'Tous les chapitres du volume ne sont pas scrapés.'], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Entity\ContentSource;
|
|||||||
use App\Form\ContentSourceType;
|
use App\Form\ContentSourceType;
|
||||||
use App\Repository\ContentSourceRepository;
|
use App\Repository\ContentSourceRepository;
|
||||||
use App\Service\MangaScraperService;
|
use App\Service\MangaScraperService;
|
||||||
|
use App\Service\NotificationService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -18,7 +19,8 @@ class SettingsController extends AbstractController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private MangaScraperService $mangaScraperService,
|
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()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$this->entityManager->persist($contentSource);
|
$this->entityManager->persist($contentSource);
|
||||||
$this->entityManager->flush();
|
$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');
|
return $this->redirectToRoute('app_settings_scrappers_list');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +96,15 @@ class SettingsController extends AbstractController
|
|||||||
$mangaSlug = $request->request->get('mangaSlug');
|
$mangaSlug = $request->request->get('mangaSlug');
|
||||||
$chapterNumber = $request->request->get('chapterNumber');
|
$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([
|
return new JsonResponse([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use App\Repository\ContentSourceRepository;
|
use App\Repository\ContentSourceRepository;
|
||||||
|
use App\Service\ChapterUrlGenerator;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: ContentSourceRepository::class)]
|
#[ORM\Entity(repositoryClass: ContentSourceRepository::class)]
|
||||||
class ContentSource
|
class ContentSource
|
||||||
{
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
@@ -83,7 +88,8 @@ class ContentSource
|
|||||||
|
|
||||||
public function getChapterUrl(string $mangaTitle, float $chapterNumber): string
|
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
|
public function getScrapingType(): ?string
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class QueueStatusSubscriber implements EventSubscriberInterface
|
|||||||
public function onPageScrapingProgress(PageScrappingProgressEvent $event): void
|
public function onPageScrapingProgress(PageScrappingProgressEvent $event): void
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
'status' => 'Page scraping progress',
|
'status' => 'scrapping.progress',
|
||||||
'chapterId' => $event->getChapterId(),
|
'chapterId' => $event->getChapterId(),
|
||||||
'pageIndex' => $event->getPageIndex(),
|
'pageIndex' => $event->getPageIndex(),
|
||||||
'totalPages' => $event->getTotalPages(),
|
'totalPages' => $event->getTotalPages(),
|
||||||
|
|||||||
20
src/Manager/Toolbar/Definition/ActivityToolbar.php
Normal file
20
src/Manager/Toolbar/Definition/ActivityToolbar.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Manager\Toolbar\Definition;
|
||||||
|
|
||||||
|
use App\Manager\Toolbar\Element\ToolbarButton;
|
||||||
|
use App\Manager\Toolbar\Element\ToolbarDivider;
|
||||||
|
|
||||||
|
class ActivityToolbar extends Toolbar
|
||||||
|
{
|
||||||
|
public function __construct(array $contextData = [])
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->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'))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,17 +10,17 @@ class ChapterListToolbar extends Toolbar
|
|||||||
public function __construct(array $contextData = [])
|
public function __construct(array $contextData = [])
|
||||||
{
|
{
|
||||||
$this
|
$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 ToolbarDivider())
|
||||||
->addToLeftGroup(new ToolbarButton('keyboard', 'Rename chapters', 'renameChapters'))
|
->addToLeftGroup(new ToolbarButton('keyboard', 'Rename chapters', 'toolbar#renameChapters'))
|
||||||
->addToLeftGroup(new ToolbarButton('file-zipper', 'Manage cbz', 'manageCbz', $contextData))
|
->addToLeftGroup(new ToolbarButton('file-zipper', 'Manage cbz', 'toolbar#manageCbz', $contextData))
|
||||||
->addToLeftGroup(new ToolbarButton('history', 'History', 'history', $contextData))
|
->addToLeftGroup(new ToolbarButton('history', 'History', 'toolbar#history', $contextData))
|
||||||
|
|
||||||
|
|
||||||
->addToRightGroup(new ToolbarButton('bookmark', 'Monitoring', 'monitoring', $contextData))
|
->addToRightGroup(new ToolbarButton('bookmark', 'Monitoring', 'toolbar#monitoring', $contextData))
|
||||||
->addToRightGroup(new ToolbarButton('wrench', 'Edit', 'editManga', $contextData))
|
->addToRightGroup(new ToolbarButton('wrench', 'Edit', 'toolbar#editManga', $contextData))
|
||||||
->addToRightGroup(new ToolbarButton('trash-can', 'Delete', 'deleteManga', $contextData))
|
->addToRightGroup(new ToolbarButton('trash-can', 'Delete', 'toolbar#deleteManga', $contextData))
|
||||||
->addToRightGroup(new ToolbarDivider())
|
->addToRightGroup(new ToolbarDivider())
|
||||||
->addToRightGroup(new ToolbarButton('chevron-down', 'Expand all', 'expandAll'));
|
->addToRightGroup(new ToolbarButton('chevron-down', 'Expand all', 'toolbar#expandAll'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Manager\Toolbar\Factory;
|
namespace App\Manager\Toolbar\Factory;
|
||||||
|
|
||||||
|
use App\Manager\Toolbar\Definition\ActivityToolbar;
|
||||||
use App\Manager\Toolbar\Definition\ChapterListToolbar;
|
use App\Manager\Toolbar\Definition\ChapterListToolbar;
|
||||||
use App\Manager\Toolbar\Definition\MangaListToolbar;
|
use App\Manager\Toolbar\Definition\MangaListToolbar;
|
||||||
use App\Manager\Toolbar\Definition\Toolbar;
|
use App\Manager\Toolbar\Definition\Toolbar;
|
||||||
@@ -13,6 +14,7 @@ class ToolbarFactory
|
|||||||
return match ($type) {
|
return match ($type) {
|
||||||
'manga_list' => new MangaListToolbar(),
|
'manga_list' => new MangaListToolbar(),
|
||||||
'chapter_list' => new ChapterListToolbar($context),
|
'chapter_list' => new ChapterListToolbar($context),
|
||||||
|
'activity' => new ActivityToolbar($context),
|
||||||
default => throw new \InvalidArgumentException("Unknown toolbar type: $type"),
|
default => throw new \InvalidArgumentException("Unknown toolbar type: $type"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/Service/ChapterUrlGenerator.php
Normal file
34
src/Service/ChapterUrlGenerator.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
|
class ChapterUrlGenerator
|
||||||
|
{
|
||||||
|
private string $chapterUrlFormat;
|
||||||
|
|
||||||
|
public function __construct(string $chapterUrlFormat)
|
||||||
|
{
|
||||||
|
$this->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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,12 +5,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto mt-8 p-2">
|
||||||
<div class="bg-white overflow-hidden">
|
<div class="bg-white overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full bg-white">
|
<table class="min-w-full bg-white">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-gray-800 text-white">
|
<tr class="bg-gray-200 text-gray-800">
|
||||||
<th class="w-1/12 py-3 px-4 text-left">
|
<th class="w-1/12 py-3 px-4 text-left">
|
||||||
<input type="checkbox" class="form-checkbox h-5 w-5 text-green-600">
|
<input type="checkbox" class="form-checkbox h-5 w-5 text-green-600">
|
||||||
</th>
|
</th>
|
||||||
@@ -18,14 +18,13 @@
|
|||||||
<th class="w-1/12 py-3 px-4 text-left">Volume</th>
|
<th class="w-1/12 py-3 px-4 text-left">Volume</th>
|
||||||
<th class="w-3/12 py-3 px-4 text-left">Chapitre</th>
|
<th class="w-3/12 py-3 px-4 text-left">Chapitre</th>
|
||||||
<th class="w-3/12 py-3 px-4 text-left">Titre</th>
|
<th class="w-3/12 py-3 px-4 text-left">Titre</th>
|
||||||
|
<th class="w-3/12 py-3 px-4 text-left">Progress</th>
|
||||||
<th class="w-2/12 py-3 px-4 text-left">Actions</th>
|
<th class="w-2/12 py-3 px-4 text-left">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-gray-700">
|
<tbody class="text-gray-700">
|
||||||
{% for manga in status %}
|
{% for manga in status %}
|
||||||
<tr class="border-b border-gray-200 hover:bg-gray-50 transition duration-150 ease-in-out"
|
<tr class="border-b border-gray-200 hover:bg-gray-50 transition duration-150 ease-in-out">
|
||||||
data-controller="chapter-progress"
|
|
||||||
data-chapter-progress-chapter-id-value="{{ manga.chapterId }}">
|
|
||||||
<td class="py-4 px-4 text-center">
|
<td class="py-4 px-4 text-center">
|
||||||
<input type="checkbox" class="form-checkbox h-5 w-5 text-green-600">
|
<input type="checkbox" class="form-checkbox h-5 w-5 text-green-600">
|
||||||
</td>
|
</td>
|
||||||
@@ -33,15 +32,21 @@
|
|||||||
<td class="py-4 px-4">{{ manga.volume }}</td>
|
<td class="py-4 px-4">{{ manga.volume }}</td>
|
||||||
<td class="py-4 px-4">
|
<td class="py-4 px-4">
|
||||||
{{ manga.chapter }}
|
{{ manga.chapter }}
|
||||||
<div class="mt-2 hidden" data-chapter-progress-target="progressBar">
|
|
||||||
<div class="bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
</td>
|
||||||
<div class="bg-green-600 h-2.5 rounded-full" style="width: 0"></div>
|
<td class="py-4 px-4">{{ manga.title }}</td>
|
||||||
</div>
|
<td class="py-4 px-4">
|
||||||
<div class="text-xs mt-1 text-center" data-chapter-progress-target="progressText">
|
<div class="mt-2"
|
||||||
|
data-controller="chapter-progress"
|
||||||
|
data-chapter-progress-chapter-id-value="{{ manga.chapterId }}">
|
||||||
|
<div class="relative bg-gray-200 rounded-full h-6 overflow-hidden">
|
||||||
|
<div data-chapter-progress-target="progressBar" class="absolute top-0 left-0 h-full bg-green-400 transition-all duration-300 ease-out" style="width: 0%"></div>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center text-xs font-semibold text-white" data-chapter-progress-target="progressText">
|
||||||
|
0 / 0
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-4">{{ manga.title }}</td>
|
|
||||||
<td class="py-4 px-4">
|
<td class="py-4 px-4">
|
||||||
<button class="text-red-500 hover:text-red-700 transition duration-150 ease-in-out">
|
<button class="text-red-500 hover:text-red-700 transition duration-150 ease-in-out">
|
||||||
<i class="fas fa-trash-alt"></i>
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
|||||||
@@ -48,7 +48,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<turbo-frame id="chapter_list" src="{{ fragment_uri(controller('App\\Controller\\MangaController::_chaptersByManga', {'id': manga.id})) }}"></turbo-frame>
|
<turbo-frame id="chapter_list"
|
||||||
|
src="{{ fragment_uri(controller('App\\Controller\\MangaController::_chaptersByManga', {'id': manga.id})) }}"></turbo-frame>
|
||||||
{# Modal d'édition #}
|
{# Modal d'édition #}
|
||||||
<twig:Modal
|
<twig:Modal
|
||||||
openTrigger="openEditModal"
|
openTrigger="openEditModal"
|
||||||
@@ -96,12 +97,13 @@
|
|||||||
</p>
|
</p>
|
||||||
</twig:block>
|
</twig:block>
|
||||||
<twig:block name="footer">
|
<twig:block name="footer">
|
||||||
<form id="deleteForm" method="post" action="">
|
<button
|
||||||
<button type="submit"
|
{{ stimulus_controller('toolbar', { mangaId: manga.id }) }}
|
||||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
|
{{ stimulus_action('toolbar', 'confirmDelete') }}
|
||||||
Delete
|
type="button"
|
||||||
</button>
|
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||||
</form>
|
Delete
|
||||||
|
</button>
|
||||||
<button type="button" data-action="modal#close"
|
<button type="button" data-action="modal#close"
|
||||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
Reference in New Issue
Block a user