Merge pull request 'chore: rattrapage' (#11) from style/sidebar-cleanup-and-ui-polish into main
All checks were successful
Deploy / deploy (push) Successful in 2m55s
All checks were successful
Deploy / deploy (push) Successful in 2m55s
Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = []
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user