feat: ajout de la gestion des chapitres dans le store Manga avec des actions pour charger et mettre à jour la disponibilité des chapitres, intégration d'un écouteur Mercure pour les mises à jour en temps réel, et amélioration des composants d'interface utilisateur pour gérer les états de chargement et d'erreur.

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-04-04 16:06:32 +02:00
parent e51712a800
commit 5928cfd5f0
11 changed files with 539 additions and 39 deletions

View File

@@ -11,14 +11,13 @@
params: {
chapterId: chapter.id
}
}"
class="text-green-500">
}">
{{ chapter.title || 'Sans titre' }}
</router-link>
<span v-else>{{ chapter.title || 'Sans titre' }}</span>
</td>
<td class="px-4 py-2 flex justify-end gap-2">
<button v-if="!chapter.isAvailable" @click="handleSearch" class="text-gray-500 hover:text-green-500">
<button v-if="!chapter.isAvailable" @click="handleSearch" :class="buttonClass">
<MagnifyingGlassIcon class="h-5 w-5" />
</button>
<button v-else @click="handleDelete" class="text-gray-500 hover:text-green-500">
@@ -35,7 +34,8 @@
</template>
<script setup>
import { MagnifyingGlassIcon, ArrowDownTrayIcon, XMarkIcon, TrashIcon } from '@heroicons/vue/24/solid';
import { ArrowDownTrayIcon, MagnifyingGlassIcon, TrashIcon, XMarkIcon } from '@heroicons/vue/24/solid';
import { computed, ref, watch } from 'vue';
import { useMangaStore } from '../../application/store/mangaStore';
const props = defineProps({
@@ -50,11 +50,38 @@
});
const store = useMangaStore();
const isLoading = ref(false);
const buttonClass = computed(() => {
return isLoading.value ? 'text-yellow-500 cursor-wait' : 'text-gray-500 hover:text-green-500';
});
// Surveiller les changements d'état du chapitre
watch(
() => props.chapter.isAvailable,
(newValue, oldValue) => {
console.log(
`MangaChapter: État du chapitre ${props.chapter.number} (ID: ${props.chapter.id}) modifié - ${oldValue} => ${newValue}`
);
// Si le chapitre devient disponible, on arrête le chargement
if (newValue === true) {
isLoading.value = false;
}
}
);
const handleSearch = async () => {
try {
console.log(`MangaChapter: Recherche du chapitre ${props.chapter.number} (ID: ${props.chapter.id})`);
// Montrer l'indicateur de chargement
isLoading.value = true;
// Lancer la recherche du chapitre - L'UI sera mise à jour par l'événement Mercure
await store.searchChapter(props.chapter.id);
} catch (error) {
// En cas d'erreur, on arrête le chargement
isLoading.value = false;
console.error('Erreur lors de la recherche du chapitre:', error);
}
};

View File

@@ -32,8 +32,14 @@
<!-- Actions du volume -->
<div class="flex space-x-2 text-xl text-bold">
<button class="w-8 text-center" @click="handleSearch">
<MagnifyingGlassIcon class="h-6 w-6 text-gray-500 hover:text-green-500" />
<button
class="w-8 text-center"
@click="handleSearch"
:class="{
'text-yellow-500 cursor-wait': isSearching,
'text-gray-500 hover:text-green-500': !isSearching
}">
<MagnifyingGlassIcon class="h-6 w-6" />
</button>
<button class="w-8 text-center" @click="handleDownload">
<ArrowDownTrayIcon class="h-6 w-6 text-gray-500 hover:text-green-500" />
@@ -54,16 +60,16 @@
</template>
<script setup>
import { ref } from 'vue';
import {
ArrowDownTrayIcon,
BookmarkIcon,
ChevronUpIcon,
ChevronDownIcon,
MagnifyingGlassIcon,
ArrowDownTrayIcon
ChevronUpIcon,
MagnifyingGlassIcon
} from '@heroicons/vue/24/outline';
import MangaChapterList from './MangaChapterList.vue';
import { ref } from 'vue';
import { useMangaStore } from '../../application/store/mangaStore';
import MangaChapterList from './MangaChapterList.vue';
const props = defineProps({
volume: {
@@ -82,14 +88,48 @@
const store = useMangaStore();
const isOpen = ref(props.isOpen);
const isSearching = ref(false);
const toggleVolume = () => {
isOpen.value = !isOpen.value;
};
const handleSearch = async () => {
// TODO: Implémenter la recherche du volume
console.log('Recherche du volume:', props.volume.number);
if (isSearching.value) return; // Éviter les clicks multiples
try {
isSearching.value = true;
console.log(
`Recherche du volume ${props.volume.number} - Lancement du scraping de ${props.volume.chapters.length} chapitres`
);
// Récupérer tous les chapitres non disponibles du volume
const chaptersToSearch = props.volume.chapters
.filter(chapter => !chapter.isAvailable)
.map(chapter => chapter.id);
if (chaptersToSearch.length === 0) {
console.log('Tous les chapitres sont déjà disponibles !');
isSearching.value = false;
return;
}
console.log(`Chapitres à scraper: ${chaptersToSearch.length}`);
// Lancer le scraping de chaque chapitre non disponible en séquentiel
for (const chapterId of chaptersToSearch) {
console.log(`Scraping du chapitre ${chapterId}...`);
await store.searchChapter(chapterId);
// Petite pause entre chaque requête pour éviter de surcharger le serveur
await new Promise(resolve => setTimeout(resolve, 500));
}
console.log(`Scraping des chapitres du volume ${props.volume.number} terminé`);
} catch (error) {
console.error(`Erreur lors du scraping du volume ${props.volume.number}:`, error);
} finally {
isSearching.value = false;
}
};
const handleDownload = async () => {

View File

@@ -0,0 +1,134 @@
<template>
<!-- Composant invisible qui écoute les événements Mercure -->
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch, inject } from 'vue';
import { useMangaStore } from '../../application/store/mangaStore';
import { useQueryClient } from '@tanstack/vue-query';
const props = defineProps({
mangaId: {
type: String,
required: true
}
});
const store = useMangaStore();
const eventSource = ref(null);
// On récupère le client de requête pour invalider le cache
const queryClient = useQueryClient();
const setupMercureEventSource = () => {
if (eventSource.value) {
eventSource.value.close();
}
// Créer les topics à écouter
const topics = [`manga/${props.mangaId}/chapters`, 'scraping/status'];
// Construire l'URL du hub Mercure avec les topics
const mercureHubUrl = new URL('/.well-known/mercure', window.location.origin);
topics.forEach(topic => {
mercureHubUrl.searchParams.append('topic', topic);
});
console.log(`MercureListener: Abonnement aux topics pour manga ${props.mangaId}`);
console.log(`MercureListener: URL Mercure - ${mercureHubUrl.toString()}`);
// Créer la source d'événements
eventSource.value = new EventSource(mercureHubUrl, { withCredentials: true });
// Définir les gestionnaires d'événements
eventSource.value.onmessage = event => {
try {
console.log('MercureListener: Événement reçu', event.data);
const data = JSON.parse(event.data);
handleMercureEvent(data);
} catch (error) {
console.error("Erreur lors du traitement de l'événement Mercure:", error);
}
};
eventSource.value.onerror = error => {
console.error('Erreur de connexion à Mercure:', error);
// Tenter de reconnecter après un délai
setTimeout(() => {
if (eventSource.value) {
setupMercureEventSource();
}
}, 5000);
};
};
const handleMercureEvent = data => {
if (!data || !data.type) {
console.warn('MercureListener: Événement sans type reçu', data);
return;
}
switch (data.type) {
case 'chapter.scraped':
console.log(`MercureListener: Chapitre ${data.chapterNumber} scrappé avec succès!`, data);
// Vérifier que l'ID du chapitre est présent et au bon format
if (!data.chapterId) {
console.error("MercureListener: ID du chapitre manquant dans l'événement", data);
return;
}
// Mettre à jour l'état du chapitre dans le store
try {
// Mettre à jour le store Pinia
store.updateChapterAvailability(data.chapterId, true);
console.log(
`MercureListener: Chapitre ${data.chapterNumber} (ID: ${data.chapterId}) marqué comme disponible`
);
// Invalider le cache des requêtes pour forcer un rechargement frais
console.log(`MercureListener: Invalidation du cache pour les chapitres du manga ${props.mangaId}`);
queryClient.invalidateQueries(['manga', ref(props.mangaId), 'chapters']);
// Force le rechargement des chapitres via le store
setTimeout(() => {
console.log('MercureListener: Rechargement forcé des chapitres via le store');
store.loadChapters(props.mangaId);
}, 100);
} catch (error) {
console.error('MercureListener: Erreur lors de la mise à jour du chapitre', error);
}
break;
case 'chapter.scraping.failed':
console.error(`MercureListener: Échec du scraping du chapitre ${data.chapterNumber}:`, data.reason);
break;
default:
console.log('MercureListener: Événement Mercure non géré:', data);
}
};
watch(
() => props.mangaId,
(newMangaId, oldMangaId) => {
console.log(`MercureListener: MangaId changé de ${oldMangaId} à ${newMangaId}`);
if (newMangaId) {
setupMercureEventSource();
} else if (eventSource.value) {
eventSource.value.close();
eventSource.value = null;
}
}
);
onMounted(() => {
setupMercureEventSource();
});
onBeforeUnmount(() => {
if (eventSource.value) {
eventSource.value.close();
eventSource.value = null;
}
});
</script>