chore: rattrapage

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2026-03-14 00:45:29 +01:00
parent 8e1c4637ba
commit 7fba3c6fcb
14 changed files with 87 additions and 23 deletions

View File

@@ -11,7 +11,10 @@ export class Manga {
status = null, status = null,
rating = null, rating = null,
genres = [], genres = [],
createdAt = new Date().toISOString() createdAt = new Date().toISOString(),
monitored = false,
chaptersTotal = 0,
chaptersScraped = 0,
}) { }) {
this.id = id; this.id = id;
this.slug = slug; this.slug = slug;
@@ -25,6 +28,9 @@ export class Manga {
this.rating = rating; this.rating = rating;
this.genres = genres; this.genres = genres;
this.createdAt = createdAt; this.createdAt = createdAt;
this.monitored = monitored;
this.chaptersTotal = chaptersTotal;
this.chaptersScraped = chaptersScraped;
} }
static create(data) { static create(data) {

View File

@@ -2,36 +2,26 @@
<RouterLink <RouterLink
:to="{ name: 'manga-details', params: { id: manga.id } }" :to="{ name: 'manga-details', params: { id: manga.id } }"
class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden cursor-pointer transition-transform hover:scale-105 block"> class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden cursor-pointer transition-transform hover:scale-105 block">
<div class="relative pb-[150%]"> <div class="relative pb-[130%]">
<img <img
:src="manga.thumbnailUrl || 'https://via.placeholder.com/300x400'" :src="manga.thumbnailUrl || 'https://via.placeholder.com/300x400'"
:alt="manga.title" :alt="manga.title"
class="absolute inset-0 w-full h-full object-cover bg-gray-100" /> class="absolute inset-0 w-full h-full object-cover bg-gray-100" />
</div> </div>
<div class="p-2"> <div class="p-2">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-1">{{ manga.title }}</h3> <h3 class="text-sm font-medium text-gray-800 dark:text-gray-100 mb-1 truncate">{{ manga.title }}</h3>
<div class="flex items-center"> <div class="flex items-center">
<span class="text-sm text-gray-500 dark:text-gray-400">{{ manga.publicationYear }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{ manga.publicationYear }}</span>
</div> </div>
<div class="mt-1 text-sm text-gray-500 dark:text-gray-400"> Added: {{ formatDate(manga.createdAt) }} </div>
</div> </div>
</RouterLink> </RouterLink>
</template> </template>
<script setup> <script setup>
const props = defineProps({ defineProps({
manga: { manga: {
type: Object, type: Object,
required: true required: true
} }
}); });
const formatDate = dateString => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 p-6"> <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 p-4">
<MangaCard v-for="manga in mangas" :key="manga.id" :manga="manga" /> <MangaCard v-for="manga in mangas" :key="manga.id" :manga="manga" />
</div> </div>
</template> </template>

View File

@@ -0,0 +1,20 @@
<template>
<span v-if="isLoading" class="text-gray-400 dark:text-gray-600 text-xs"></span>
<span v-else-if="sources.length" class="text-gray-700 dark:text-gray-300 truncate max-w-xs block">{{ sources[0].name }}</span>
<span v-else class="text-gray-400 dark:text-gray-600"></span>
</template>
<script setup>
import { computed, toRef } from 'vue';
import { useMangaPreferredSources } from '../composables/useMangaPreferredSources';
const props = defineProps({
mangaId: {
type: String,
required: true
}
});
const mangaIdRef = toRef(props, 'mangaId');
const { sources, isLoading } = useMangaPreferredSources(mangaIdRef);
</script>

View File

@@ -64,6 +64,11 @@
@click="store.setDefaultView('list')"> @click="store.setDefaultView('list')">
{{ t('preferences.defaultView.list') }} {{ t('preferences.defaultView.list') }}
</button> </button>
<button
:class="viewButtonClass('table')"
@click="store.setDefaultView('table')">
{{ t('preferences.defaultView.table') }}
</button>
</div> </div>
</div> </div>
<!-- Mangas par page --> <!-- Mangas par page -->

View File

@@ -27,7 +27,8 @@
"defaultView": { "defaultView": {
"label": "Default view", "label": "Default view",
"grid": "Grid", "grid": "Grid",
"list": "List" "list": "List",
"table": "Table"
}, },
"itemsPerPage": { "itemsPerPage": {
"label": "Mangas per page" "label": "Mangas per page"

View File

@@ -27,7 +27,8 @@
"defaultView": { "defaultView": {
"label": "Vue par défaut", "label": "Vue par défaut",
"grid": "Grille", "grid": "Grille",
"list": "Liste" "list": "Liste",
"table": "Tableau"
}, },
"itemsPerPage": { "itemsPerPage": {
"label": "Mangas par page" "label": "Mangas par page"

View File

@@ -24,11 +24,21 @@ readonly class GetMangaListHandler
$total = $this->mangaRepository->count(); $total = $this->mangaRepository->count();
$chapterCounts = [];
foreach ($mangas as $manga) {
$id = $manga->getId()->getValue();
$chapterCounts[$id] = [
'total' => $this->mangaRepository->countChapters($id),
'scraped' => $this->mangaRepository->countAvailableChapters($id),
];
}
return new MangaListResponse( return new MangaListResponse(
mangas: $mangas, mangas: $mangas,
total: $total, total: $total,
page: $query->page, page: $query->page,
limit: $query->limit limit: $query->limit,
chapterCounts: $chapterCounts
); );
} }
} }

View File

@@ -8,7 +8,8 @@ readonly class MangaListResponse
public array $mangas, public array $mangas,
public int $total, public int $total,
public int $page, public int $page,
public int $limit public int $limit,
public array $chapterCounts = []
) { ) {
} }

View File

@@ -31,6 +31,7 @@ interface MangaRepositoryInterface
public function findChapters(string $mangaId, int $page = 1, int $limit = 20, string $sortOrder = 'desc'): array; public function findChapters(string $mangaId, int $page = 1, int $limit = 20, string $sortOrder = 'desc'): array;
public function countChapters(string $mangaId): int; public function countChapters(string $mangaId): int;
public function countAvailableChapters(string $mangaId): int;
public function findChapterById(string $id): ?Chapter; public function findChapterById(string $id): ?Chapter;
public function findVisibleChapterById(string $id): ?Chapter; public function findVisibleChapterById(string $id): ?Chapter;
public function findChapterByMangaIdAndNumber(string $mangaId, float $chapterNumber): ?Chapter; public function findChapterByMangaIdAndNumber(string $mangaId, float $chapterNumber): ?Chapter;

View File

@@ -21,6 +21,9 @@ readonly class MangaListItem
public string $status, public string $status,
public ?float $rating, public ?float $rating,
public DateTimeImmutable $createdAt, public DateTimeImmutable $createdAt,
public bool $monitored = false,
public int $chaptersTotal = 0,
public int $chaptersScraped = 0,
) { ) {
} }
} }

View File

@@ -29,7 +29,10 @@ readonly class GetMangaListStateProvider implements ProviderInterface
return new MangaCollection( return new MangaCollection(
items: array_map( items: array_map(
fn (Manga $manga) => $this->createMangaListItem($manga), fn (Manga $manga) => $this->createMangaListItem(
$manga,
$response->chapterCounts[$manga->getId()->getValue()] ?? []
),
$response->mangas $response->mangas
), ),
total: $response->total, total: $response->total,
@@ -40,7 +43,7 @@ readonly class GetMangaListStateProvider implements ProviderInterface
); );
} }
private function createMangaListItem(Manga $manga): MangaListItem private function createMangaListItem(Manga $manga, array $counts = []): MangaListItem
{ {
return new MangaListItem( return new MangaListItem(
id: $manga->getId()->getValue(), id: $manga->getId()->getValue(),
@@ -54,7 +57,10 @@ readonly class GetMangaListStateProvider implements ProviderInterface
genres: $manga->getGenres(), genres: $manga->getGenres(),
status: $manga->getStatus(), status: $manga->getStatus(),
rating: $manga->getRating(), rating: $manga->getRating(),
createdAt: $manga->getCreatedAt() createdAt: $manga->getCreatedAt(),
monitored: $manga->getMonitoringStatus()->isEnabled(),
chaptersTotal: $counts['total'] ?? 0,
chaptersScraped: $counts['scraped'] ?? 0,
); );
} }
} }

View File

@@ -196,6 +196,18 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
->getSingleScalarResult(); ->getSingleScalarResult();
} }
public function countAvailableChapters(string $mangaId): int
{
return $this->entityManager->createQueryBuilder()
->select('COUNT(c.id)')
->from(EntityChapter::class, 'c')
->where('c.manga = :mangaId')
->andWhere('c.pagesDirectory IS NOT NULL OR c.cbzPath IS NOT NULL')
->setParameter('mangaId', $mangaId)
->getQuery()
->getSingleScalarResult();
}
public function findByExternalId(ExternalId $externalId): ?DomainManga public function findByExternalId(ExternalId $externalId): ?DomainManga
{ {
$entity = $this->entityManager->getRepository(EntityManga::class)->findOneBy([ $entity = $this->entityManager->getRepository(EntityManga::class)->findOneBy([

View File

@@ -135,6 +135,14 @@ class InMemoryMangaRepository implements MangaRepositoryInterface
return count($this->chapters[$mangaId] ?? []); return count($this->chapters[$mangaId] ?? []);
} }
public function countAvailableChapters(string $mangaId): int
{
return count(array_filter(
$this->chapters[$mangaId] ?? [],
fn (Chapter $c) => $c->isAvailable()
));
}
public function findChapterById(string $id): ?Chapter public function findChapterById(string $id): ?Chapter
{ {
return $this->chaptersById[$id] ?? null; return $this->chaptersById[$id] ?? null;