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:
parent
e51712a800
commit
5928cfd5f0
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user