Added:
- toogle chapter visibility - delete chapter cbz - preferred ContentSource.php and modal - minor fixes
This commit is contained in:
101
assets/controllers/preferred_sources_controller.js
Normal file
101
assets/controllers/preferred_sources_controller.js
Normal file
@@ -0,0 +1,101 @@
|
||||
// assets/controllers/preferred-sources_controller.js
|
||||
|
||||
import {Controller} from "@hotwired/stimulus"
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["preferredList", "availableList"]
|
||||
static values = {
|
||||
mangaId: Number,
|
||||
preferredSources: Array,
|
||||
allSources: Array
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.initSortable()
|
||||
}
|
||||
|
||||
initSortable() {
|
||||
new Sortable(this.preferredListTarget, {
|
||||
animation: 150,
|
||||
ghostClass: 'bg-gray-300',
|
||||
onEnd: this.handleDragEnd.bind(this)
|
||||
})
|
||||
}
|
||||
|
||||
handleDragEnd() {
|
||||
this.updatePreferredSources()
|
||||
}
|
||||
|
||||
addSource(event) {
|
||||
const sourceId = parseInt(event.currentTarget.dataset.sourceId)
|
||||
if (!this.preferredSourcesValue.includes(sourceId)) {
|
||||
this.preferredSourcesValue = [...this.preferredSourcesValue, sourceId]
|
||||
this.updateLists()
|
||||
this.save()
|
||||
}
|
||||
}
|
||||
|
||||
removeSource(event) {
|
||||
const sourceId = parseInt(event.currentTarget.dataset.sourceId)
|
||||
this.preferredSourcesValue = this.preferredSourcesValue.filter(id => id !== sourceId)
|
||||
this.updateLists()
|
||||
this.save()
|
||||
}
|
||||
|
||||
updatePreferredSources() {
|
||||
this.preferredSourcesValue = Array.from(this.preferredListTarget.children).map(li => parseInt(li.dataset.id))
|
||||
this.save()
|
||||
}
|
||||
|
||||
updateLists() {
|
||||
this.preferredListTarget.innerHTML = this.preferredSourcesValue
|
||||
.map(id => this.allSourcesValue.find(s => s.id === id))
|
||||
.map(source => this.sourceTemplate(source, true))
|
||||
.join('')
|
||||
|
||||
this.availableListTarget.innerHTML = this.allSourcesValue
|
||||
.filter(source => !this.preferredSourcesValue.includes(source.id))
|
||||
.map(source => this.sourceTemplate(source, false))
|
||||
.join('')
|
||||
|
||||
this.initSortable()
|
||||
}
|
||||
|
||||
sourceTemplate(source, isPreferred) {
|
||||
return `
|
||||
<li data-id="${source.id}" draggable="true" class="flex items-center justify-between p-2 bg-gray-100 rounded ${isPreferred ? 'cursor-move' : ''}">
|
||||
<span>${source.name}</span>
|
||||
<button type="button" data-action="preferred-sources#${isPreferred ? 'removeSource' : 'addSource'}" data-source-id="${source.id}" class="text-${isPreferred ? 'red' : 'green'}-500 hover:text-${isPreferred ? 'red' : 'green'}-700">
|
||||
<i class="fas fa-${isPreferred ? 'times' : 'plus'}"></i>
|
||||
</button>
|
||||
</li>
|
||||
`
|
||||
}
|
||||
|
||||
async save() {
|
||||
try {
|
||||
const response = await fetch(`/manga/${this.mangaIdValue}/preferred-sources`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
preferredSources: this.preferredSourcesValue
|
||||
})
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Preferred sources saved successfully')
|
||||
// Optionally show a success message
|
||||
} else {
|
||||
console.error('Error saving preferred sources')
|
||||
// Optionally show an error message
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
// Optionally show an error message
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,11 @@ export default class extends Controller {
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
editPreferredSources() {
|
||||
const event = new CustomEvent('openPreferredSourcesModal');
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
deleteMangas() {
|
||||
console.log("Deleting mangas...");
|
||||
}
|
||||
|
||||
38
migrations/Version20240721172633.php
Normal file
38
migrations/Version20240721172633.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20240721172633 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE manga_content_source (manga_id INT NOT NULL, content_source_id INT NOT NULL, PRIMARY KEY(manga_id, content_source_id))');
|
||||
$this->addSql('CREATE INDEX IDX_2C40CB647B6461 ON manga_content_source (manga_id)');
|
||||
$this->addSql('CREATE INDEX IDX_2C40CB64A5DB5462 ON manga_content_source (content_source_id)');
|
||||
$this->addSql('ALTER TABLE manga_content_source ADD CONSTRAINT FK_2C40CB647B6461 FOREIGN KEY (manga_id) REFERENCES manga (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE manga_content_source ADD CONSTRAINT FK_2C40CB64A5DB5462 FOREIGN KEY (content_source_id) REFERENCES content_source (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
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('ALTER TABLE manga_content_source DROP CONSTRAINT FK_2C40CB647B6461');
|
||||
$this->addSql('ALTER TABLE manga_content_source DROP CONSTRAINT FK_2C40CB64A5DB5462');
|
||||
$this->addSql('DROP TABLE manga_content_source');
|
||||
}
|
||||
}
|
||||
32
migrations/Version20240721190312.php
Normal file
32
migrations/Version20240721190312.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20240721190312 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE chapter ADD visible BOOLEAN NOT NULL DEFAULT TRUE');
|
||||
}
|
||||
|
||||
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('ALTER TABLE chapter DROP visible');
|
||||
}
|
||||
}
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"bootstrap": "^5.3.3",
|
||||
"postcss-loader": "^7.1.0",
|
||||
"puppeteer": "^22.10.0",
|
||||
"sortablejs": "^1.15.2",
|
||||
"tailwindcss": "^3.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -9157,6 +9158,11 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz",
|
||||
"integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA=="
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -10661,6 +10667,7 @@
|
||||
}
|
||||
},
|
||||
"vendor/symfony/ux-turbo/assets": {
|
||||
"name": "@symfony/ux-turbo",
|
||||
"version": "0.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"bootstrap": "^5.3.3",
|
||||
"postcss-loader": "^7.1.0",
|
||||
"puppeteer": "^22.10.0",
|
||||
"sortablejs": "^1.15.2",
|
||||
"tailwindcss": "^3.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Manager\Toolbar\Factory\ToolbarFactory;
|
||||
use App\Message\DownloadChapter;
|
||||
use App\Message\RefreshMetadata;
|
||||
use App\Repository\ChapterRepository;
|
||||
use App\Repository\ContentSourceRepository;
|
||||
use App\Repository\MangaRepository;
|
||||
use App\Service\CbzService;
|
||||
use App\Service\MangadexProvider;
|
||||
@@ -46,7 +47,8 @@ class MangaController extends AbstractController
|
||||
private readonly MangadexProvider $mangadexProvider,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly NotificationService $notificationService,
|
||||
private readonly SluggerInterface $slugger
|
||||
private readonly SluggerInterface $slugger,
|
||||
private readonly ContentSourceRepository $contentSourceRepository
|
||||
)
|
||||
{
|
||||
$this->imageManager = new ImageManager(new Driver());
|
||||
@@ -81,11 +83,13 @@ class MangaController extends AbstractController
|
||||
}
|
||||
|
||||
$form = $this->createForm(MangaEditType::class, $manga);
|
||||
$contentSources = $this->contentSourceRepository->findAll();
|
||||
|
||||
return $this->render('manga/show_chapters.html.twig', [
|
||||
'manga' => $manga,
|
||||
'toolbar' => $this->toolbarFactory->createToolbar('chapter_list', ['mangaId' => $manga->getId(), 'isMonitored' => (int)$manga->isMonitored()])->getGroups(),
|
||||
'form' => $form->createView(),
|
||||
'contentSources' => $contentSources,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -94,6 +98,7 @@ class MangaController extends AbstractController
|
||||
{
|
||||
try {
|
||||
foreach ($manga->getChapters() as $chapter) {
|
||||
file_exists($chapter->getCbzPath()) ?? unlink($chapter->getCbzPath());
|
||||
$this->entityManager->remove($chapter);
|
||||
}
|
||||
$this->entityManager->remove($manga);
|
||||
@@ -125,6 +130,30 @@ class MangaController extends AbstractController
|
||||
return new JsonResponse(['errors' => $errors], 400);
|
||||
}
|
||||
|
||||
#[Route('/manga/{id}/preferred-sources', name: 'manga_preferred_sources', methods: ['POST'])]
|
||||
public function updatePreferredSources(
|
||||
Request $request,
|
||||
Manga $manga,
|
||||
ContentSourceRepository $contentSourceRepository
|
||||
): JsonResponse
|
||||
{
|
||||
$data = json_decode($request->getContent(), true);
|
||||
$preferredSourceIds = $data['preferredSources'] ?? [];
|
||||
|
||||
$preferredSources = $contentSourceRepository->findBy(['id' => $preferredSourceIds]);
|
||||
|
||||
// This will maintain the order of the sources as they were sent in the request
|
||||
$orderedPreferredSources = array_map(
|
||||
fn($id) => current(array_filter($preferredSources, fn($s) => $s->getId() == $id)),
|
||||
$preferredSourceIds
|
||||
);
|
||||
|
||||
$manga->setPreferredSources(array_filter($orderedPreferredSources));
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(['success' => true]);
|
||||
}
|
||||
|
||||
public function _chaptersByManga(int $id): Response
|
||||
{
|
||||
$manga = $this->mangaRepository->find($id);
|
||||
@@ -157,6 +186,33 @@ class MangaController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/delete_cbz/{id}', name: 'app_delete_cbz')]
|
||||
public function deleteChapterCbz(Chapter $chapter): JsonResponse
|
||||
{
|
||||
$cbzPath = $chapter->getCbzPath();
|
||||
if (!$cbzPath) {
|
||||
return new JsonResponse(['error' => 'No CBZ path for this chapter.'], 400);
|
||||
}
|
||||
|
||||
file_exists($cbzPath) ?? unlink($cbzPath);
|
||||
|
||||
$chapter->setCbzPath(null);
|
||||
$this->entityManager->persist($chapter);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(['success' => 'CBZ file deleted.'], 200);
|
||||
}
|
||||
|
||||
#[Route('/hide_chapter/{id}', name: 'app_hide_chapter')]
|
||||
public function hideChapter(Chapter $chapter): JsonResponse
|
||||
{
|
||||
$chapter->setVisible(false);
|
||||
$this->entityManager->persist($chapter);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(['success' => 'Chapter hidden.'], 200);
|
||||
}
|
||||
|
||||
#[Route('/manga/read/{mangaSlug}/{chapterNumber}/{pageNumber}', name: 'app_manga_read')]
|
||||
public function readChapterPage(string $mangaSlug, float $chapterNumber, int $pageNumber = 1): Response
|
||||
{
|
||||
@@ -171,17 +227,7 @@ class MangaController extends AbstractController
|
||||
}
|
||||
|
||||
if (is_null($chapter->getCbzPath())) {
|
||||
$currentPage = $chapter->getPageByNumber($pageNumber);
|
||||
if (!$currentPage) {
|
||||
throw $this->createNotFoundException("La page demandée n'existe pas.");
|
||||
}
|
||||
|
||||
return $this->render('manga/manga_reader.html.twig', [
|
||||
'manga' => $manga,
|
||||
'chapter' => $chapter,
|
||||
'pages' => $chapter->getPagesLink(),
|
||||
'currentPage' => $currentPage,
|
||||
]);
|
||||
throw $this->createNotFoundException("Le chapitre demandé n'a pas été scrapé.");
|
||||
}
|
||||
|
||||
$pageContent = $this->cbzService->getPageContent($chapter->getCbzPath(), $pageNumber);
|
||||
@@ -226,7 +272,9 @@ class MangaController extends AbstractController
|
||||
->setAuthor($request->request->get('author'))
|
||||
->setPublicationYear($request->request->get('publicationYear'))
|
||||
->setRating($request->request->get('rating'))
|
||||
->setExternalId($request->request->get('externalId'));
|
||||
->setExternalId($request->request->get('externalId'))
|
||||
->setMonitored(false)
|
||||
;
|
||||
|
||||
// Traitement de l'image
|
||||
$imageUrl = $request->request->get('imageUrl');
|
||||
@@ -327,7 +375,8 @@ class MangaController extends AbstractController
|
||||
|
||||
$volumeChapters = $this->chapterRepository->findBy([
|
||||
'manga' => $manga,
|
||||
'volume' => $volume
|
||||
'volume' => $volume,
|
||||
'visible' => true
|
||||
]);
|
||||
|
||||
if (empty($volumeChapters)) {
|
||||
@@ -374,7 +423,8 @@ class MangaController extends AbstractController
|
||||
|
||||
$volumeChapters = $this->chapterRepository->findBy([
|
||||
'manga' => $manga,
|
||||
'volume' => $volume
|
||||
'volume' => $volume,
|
||||
'visible' => true
|
||||
]);
|
||||
|
||||
if (empty($volumeChapters)) {
|
||||
|
||||
@@ -45,6 +45,9 @@ class Chapter
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $cbzPath = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?bool $visible = true;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pagesLink = new ArrayCollection();
|
||||
@@ -194,4 +197,16 @@ class Chapter
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isVisible(): ?bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
public function setVisible(bool $visible): static
|
||||
{
|
||||
$this->visible = $visible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,4 +118,13 @@ class ContentSource
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCleanBaseUrl(): string
|
||||
{
|
||||
return preg_replace(
|
||||
'/^(https?:\/\/)?(www\.)?|\/+$/',
|
||||
'',
|
||||
$this->baseUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,10 +62,14 @@ class Manga
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
private ?array $AlternativeSlugs = null;
|
||||
|
||||
#[ORM\ManyToMany(targetEntity: ContentSource::class)]
|
||||
private Collection $preferredSources;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->chapters = new ArrayCollection();
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
$this->preferredSources = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -280,4 +284,38 @@ class Manga
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ContentSource>
|
||||
*/
|
||||
public function getPreferredSources(): Collection
|
||||
{
|
||||
return $this->preferredSources;
|
||||
}
|
||||
|
||||
public function addPreferredSource(ContentSource $preferredSource): static
|
||||
{
|
||||
if (!$this->preferredSources->contains($preferredSource)) {
|
||||
$this->preferredSources->add($preferredSource);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePreferredSource(ContentSource $preferredSource): static
|
||||
{
|
||||
$this->preferredSources->removeElement($preferredSource);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPreferredSources(array $sources): self
|
||||
{
|
||||
$this->preferredSources->clear();
|
||||
foreach ($sources as $source) {
|
||||
$this->addPreferredSource($source);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class ChapterListToolbar extends Toolbar
|
||||
->addToLeftGroup(new ToolbarDivider())
|
||||
->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))
|
||||
->addToLeftGroup(new ToolbarButton('gear', 'Preferred Sources', 'toolbar#editPreferredSources', $contextData))
|
||||
|
||||
|
||||
->addToRightGroup(new ToolbarButton('bookmark', $monitoredTitle, 'toolbar#monitoring', array_merge($contextData, ['buttonClass' => $monitoredColor])))
|
||||
|
||||
@@ -6,8 +6,8 @@ use App\Entity\ContentSource;
|
||||
use App\Message\DownloadChapter;
|
||||
use App\Repository\ChapterRepository;
|
||||
use App\Repository\ContentSourceRepository;
|
||||
use App\Service\MangaScraperService;
|
||||
use App\Service\NotificationService;
|
||||
use App\Service\Scraper\MangaScraperService;
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
@@ -40,7 +40,16 @@ readonly class DownloadChapterHandler
|
||||
throw new BadRequestHttpException('Chapter already downloaded');
|
||||
}
|
||||
|
||||
$sources = $this->contentSourceRepository->findAll();
|
||||
$manga = $chapter->getManga();
|
||||
$preferredSources = $manga->getPreferredSources()->toArray();
|
||||
$allSources = $this->contentSourceRepository->findAll();
|
||||
|
||||
$filteredSources = array_udiff($allSources, $preferredSources, function ($a, $b) {
|
||||
return $a->getId() - $b->getId();
|
||||
});
|
||||
|
||||
$sources = array_merge($preferredSources, $filteredSources);
|
||||
|
||||
$sources[] =
|
||||
(new ContentSource())
|
||||
->setBaseUrl('https://api.mangadex.org/')
|
||||
|
||||
@@ -227,7 +227,6 @@ readonly class MangadexProvider implements MetadataProviderInterface
|
||||
$mergedChapters = [];
|
||||
foreach ($allChapters as $chapter) {
|
||||
$number = $chapter->getNumber();
|
||||
|
||||
$existingChapter = $manga->getChapterByNumber($number);
|
||||
if ($existingChapter) {
|
||||
if ($existingChapter->getExternalId() !== $chapter->getExternalId() && is_null($existingChapter->getExternalId())) {
|
||||
|
||||
@@ -37,6 +37,7 @@ class JavascriptScraper extends AbstractScraper
|
||||
$imagePath = $tempDir . '/' . $imageName;
|
||||
|
||||
file_put_contents($imagePath, file_get_contents($page['image_url']));
|
||||
$this->dispatchProgressEvent($chapter, $index + 1, count($pageData));
|
||||
|
||||
$page['local_image_url'] = $imagePath;
|
||||
}
|
||||
@@ -95,7 +96,7 @@ class JavascriptScraper extends AbstractScraper
|
||||
{
|
||||
$chapterSelector = $contentSource->getChapterSelector();
|
||||
if (!$chapterSelector) {
|
||||
return; // Si aucun sélecteur n'est défini, on ne fait rien
|
||||
return;
|
||||
}
|
||||
|
||||
$crawler = $pantherClient->waitFor($chapterSelector);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% set volume_cbz_path = chapters|first.cbzPath %}
|
||||
{% set all_chapters_same_cbz = chapters|reduce((carry, chapter) => carry and chapter.cbzPath == volume_cbz_path, true) %}
|
||||
{% set available_chapters = chapters|filter(chapter => chapter.cbzPath is not null) %}
|
||||
{% set total_chapters = chapters|length %}
|
||||
{% set total_chapters = chapters|filter(chapter => chapter.visible)|length %}
|
||||
|
||||
<div data-controller="table" data-table-open-value="{{ is_first ? 'true' : 'false' }}">
|
||||
<div class="bg-white rounded-sm shadow mb-4">
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
{% if chapter.visible %}
|
||||
<tr id="chapter-{{ chapter.id }}" class="border-t hover:bg-green-100">
|
||||
{% if chapter.cbzPath is not null %}
|
||||
<td class="px-4 py-2 text-green-500">
|
||||
<a data-turbo-frame="_top" href="{{ path('app_manga_read', { mangaSlug: manga.slug, chapterNumber: chapter.number, pageNumber: 1 }) }}">
|
||||
{{ '%02d'|format(chapter.number) }}
|
||||
<a data-turbo-frame="_top"
|
||||
href="{{ path('app_manga_read', { mangaSlug: manga.slug, chapterNumber: chapter.number, pageNumber: 1 }) }}">
|
||||
{{ chapter.number < 10 ? '0' ~ chapter.number : chapter.number }}
|
||||
</a>
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="px-4 py-2">{{ '%02d'|format(chapter.number) }}</td>
|
||||
<td class="px-4 py-2">{{ chapter.number < 10 ? '0' ~ chapter.number : chapter.number }}</td>
|
||||
{% endif %}
|
||||
|
||||
<td class="px-4 py-2 w-full text-left">
|
||||
{% if chapter.cbzPath is not null %}
|
||||
<a data-turbo-frame="_top" href="{{ path('app_manga_read', { mangaSlug: manga.slug, chapterNumber: chapter.number, pageNumber: 1 }) }}">
|
||||
<a data-turbo-frame="_top"
|
||||
href="{{ path('app_manga_read', { mangaSlug: manga.slug, chapterNumber: chapter.number, pageNumber: 1 }) }}">
|
||||
{{ chapter.title ?? 'No title' }}
|
||||
</a>
|
||||
{% else %}
|
||||
@@ -30,9 +33,13 @@
|
||||
</span>
|
||||
</button>
|
||||
{% else %}
|
||||
<button disabled>
|
||||
<span class="text-gray-500">
|
||||
<i class="fas fa-search"></i>
|
||||
<button
|
||||
data-controller="download"
|
||||
data-action="download#download"
|
||||
data-download-url-value="{{ path('app_delete_cbz', {id: chapter.id}) }}"
|
||||
>
|
||||
<span class="text-gray-500 hover:text-green-500">
|
||||
<i data-download-target="icon" class="fas fa-times"></i>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -44,5 +51,15 @@
|
||||
<i data-download-target="icon"
|
||||
class="fas fa-download text-gray-500 hover:text-green-500"></i>
|
||||
</a>
|
||||
<button
|
||||
data-controller="download"
|
||||
data-action="download#download"
|
||||
data-download-url-value="{{ path('app_hide_chapter', {id: chapter.id}) }}"
|
||||
>
|
||||
<span class="text-gray-500 hover:text-green-500">
|
||||
<i data-download-target="icon" class="fas fa-trash-can"></i>
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
@@ -49,7 +49,9 @@
|
||||
</div>
|
||||
|
||||
<turbo-frame id="chapter_list"
|
||||
src="{{ fragment_uri(controller('App\\Controller\\MangaController::_chaptersByManga', {'id': manga.id})) }}"></turbo-frame>
|
||||
src="{{ fragment_uri(controller('App\\Controller\\MangaController::_chaptersByManga', {'id': manga.id})) }}"
|
||||
>
|
||||
</turbo-frame>
|
||||
{# Modal d'édition #}
|
||||
<twig:Modal
|
||||
openTrigger="openEditModal"
|
||||
@@ -81,21 +83,26 @@
|
||||
<label class="block text-sm font-medium text-gray-700">Slugs alternatifs</label>
|
||||
<div data-collection-target="container" class="grid grid-cols-4 gap-2 mt-1">
|
||||
{% for slug in form.alternativeSlugs %}
|
||||
<div class="inline-flex items-center bg-gray-100 rounded-full px-3 py-1 text-sm collection-item">
|
||||
<div
|
||||
class="inline-flex items-center bg-gray-100 rounded-full px-3 py-1 text-sm collection-item">
|
||||
{{ form_widget(slug, {'attr': {'class': 'bg-transparent border-none focus:outline-none focus:border-b focus:border-green-500 p-0 w-full'}}) }}
|
||||
<button type="button" data-action="collection#remove" class="ml-2 text-gray-500 hover:text-green-500 flex-shrink-0">
|
||||
<button type="button" data-action="collection#remove"
|
||||
class="ml-2 text-gray-500 hover:text-green-500 flex-shrink-0">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" data-action="collection#add" class="mt-2 text-sm text-green-500 hover:text-green-700">
|
||||
<button type="button" data-action="collection#add"
|
||||
class="mt-2 text-sm text-green-500 hover:text-green-700">
|
||||
+ Ajouter un slug alternatif
|
||||
</button>
|
||||
<template data-collection-target="template">
|
||||
<div class="inline-flex items-center bg-gray-100 rounded-full px-3 py-1 text-sm collection-item">
|
||||
<div
|
||||
class="inline-flex items-center bg-gray-100 rounded-full px-3 py-1 text-sm collection-item">
|
||||
{{ form_widget(form.alternativeSlugs.vars.prototype, {'attr': {'class': 'bg-transparent border-none focus:outline-none focus:border-b focus:border-green-500 p-0 w-full'}}) }}
|
||||
<button type="button" data-action="collection#remove" class="ml-2 text-gray-500 hover:text-green-500 flex-shrink-0">
|
||||
<button type="button" data-action="collection#remove"
|
||||
class="ml-2 text-gray-500 hover:text-green-500 flex-shrink-0">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -106,21 +113,26 @@
|
||||
<label class="block text-sm font-medium text-gray-700">{{ form_label(form.genres) }}</label>
|
||||
<div data-collection-target="container" class="grid grid-cols-4 gap-2 mt-1">
|
||||
{% for genre in form.genres %}
|
||||
<div class="inline-flex items-center bg-gray-100 rounded-full px-3 py-1 text-sm collection-item">
|
||||
<div
|
||||
class="inline-flex items-center bg-gray-100 rounded-full px-3 py-1 text-sm collection-item">
|
||||
{{ form_widget(genre, {'attr': {'class': 'bg-transparent border-none focus:outline-none focus:border-b focus:border-green-500 p-0 w-full'}}) }}
|
||||
<button type="button" data-action="collection#remove" class="ml-2 text-gray-500 hover:text-green-500 flex-shrink-0">
|
||||
<button type="button" data-action="collection#remove"
|
||||
class="ml-2 text-gray-500 hover:text-green-500 flex-shrink-0">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" data-action="collection#add" class="mt-2 text-sm text-green-500 hover:text-green-700">
|
||||
<button type="button" data-action="collection#add"
|
||||
class="mt-2 text-sm text-green-500 hover:text-green-700">
|
||||
+ Ajouter un genre
|
||||
</button>
|
||||
<template data-collection-target="template">
|
||||
<div class="inline-flex items-center bg-gray-100 rounded-full px-3 py-1 text-sm collection-item">
|
||||
<div
|
||||
class="inline-flex items-center bg-gray-100 rounded-full px-3 py-1 text-sm collection-item">
|
||||
{{ form_widget(form.genres.vars.prototype, {'attr': {'class': 'bg-transparent border-none focus:outline-none focus:border-b focus:border-green-500 p-0 w-full'}}) }}
|
||||
<button type="button" data-action="collection#remove" class="ml-2 text-gray-500 hover:text-green-500 flex-shrink-0">
|
||||
<button type="button" data-action="collection#remove"
|
||||
class="ml-2 text-gray-500 hover:text-green-500 flex-shrink-0">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -166,5 +178,63 @@
|
||||
</button>
|
||||
</twig:block>
|
||||
</twig:Modal>
|
||||
|
||||
<twig:Modal
|
||||
openTrigger="openPreferredSourcesModal"
|
||||
closeTrigger="closePreferredSourcesModal"
|
||||
title="Manage Preferred Sources"
|
||||
modalClass="w-full max-w-4xl"
|
||||
>
|
||||
{% block content %}
|
||||
<div {{ stimulus_controller('preferred-sources', {
|
||||
mangaId: manga.id,
|
||||
preferredSources: manga.preferredSources|map(s => s.id)|json_encode,
|
||||
allSources: contentSources|map(s => {
|
||||
id: s.id,
|
||||
name: s.cleanBaseUrl
|
||||
})|json_encode
|
||||
}) }}>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">Preferred Sources</h3>
|
||||
<ul data-preferred-sources-target="preferredList" class="mt-2 space-y-2">
|
||||
{% for source in manga.preferredSources %}
|
||||
<li data-id="{{ source.id }}" draggable="true"
|
||||
class="flex items-center justify-between p-2 bg-gray-100 rounded cursor-move">
|
||||
<span>{{ source.cleanBaseUrl }}</span>
|
||||
<button type="button" data-action="preferred-sources#removeSource"
|
||||
data-source-id="{{ source.id }}" class="text-red-500 hover:text-red-700">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">Available Sources</h3>
|
||||
<ul data-preferred-sources-target="availableList" class="mt-2 space-y-2">
|
||||
{% for source in contentSources %}
|
||||
{% if source not in manga.preferredSources %}
|
||||
<li class="flex items-center justify-between p-2 bg-gray-100 rounded">
|
||||
<span>{{ source.cleanBaseUrl }}</span>
|
||||
<button type="button" data-action="preferred-sources#addSource"
|
||||
data-source-id="{{ source.id }}"
|
||||
class="text-green-500 hover:text-green-700">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
<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">
|
||||
Close
|
||||
</button>
|
||||
{% endblock %}
|
||||
</twig:Modal>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user