feat: ajout de la fonctionnalité de monitoring des mangas, incluant l'activation et la désactivation du suivi, la synchronisation des chapitres, et la mise à jour de l'API pour gérer ces nouvelles actions. Création de nouveaux composants Vue pour le rafraîchissement des chapitres et l'affichage des notifications. Intégration de tests unitaires pour valider le bon fonctionnement de ces fonctionnalités.
This commit is contained in:
parent
d9e78b5229
commit
00d63dffeb
@@ -195,11 +195,37 @@ export const useMangaStore = defineStore('manga', {
|
||||
this.chaptersError = null;
|
||||
|
||||
try {
|
||||
// Déclenche la récupération initiale des chapitres depuis la source externe
|
||||
await mangaRepository.fetchMangaChapters(mangaId);
|
||||
this.mangaChapters[mangaId] = chaptersData;
|
||||
console.log('Chapitres récupérés avec succès');
|
||||
console.log('Récupération initiale des chapitres déclenchée avec succès');
|
||||
|
||||
// Note: Les nouveaux chapitres seront disponibles après traitement asynchrone
|
||||
// Le MercureListener se chargera de mettre à jour l'interface
|
||||
} catch (err) {
|
||||
this.chaptersError = err.message;
|
||||
console.error('Erreur lors de la récupération des chapitres:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
this.loadingChapters = false;
|
||||
}
|
||||
},
|
||||
|
||||
async refreshMangaChapters(mangaId) {
|
||||
if (this.loadingChapters) return;
|
||||
this.loadingChapters = true;
|
||||
this.chaptersError = null;
|
||||
|
||||
try {
|
||||
// Déclenche la synchronisation incrémentale avec scraping automatique
|
||||
await mangaRepository.refreshMangaChapters(mangaId);
|
||||
console.log('Synchronisation incrémentale déclenchée avec succès');
|
||||
|
||||
// Note: Les chapitres mis à jour seront disponibles après traitement asynchrone
|
||||
// Le MercureListener se chargera de mettre à jour l'interface
|
||||
} catch (err) {
|
||||
this.chaptersError = err.message;
|
||||
console.error('Erreur lors de la synchronisation des chapitres:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
this.loadingChapters = false;
|
||||
}
|
||||
|
||||
@@ -142,6 +142,26 @@ export class ApiMangaRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async refreshMangaChapters(mangaId) {
|
||||
try {
|
||||
const response = await fetch(`/api/manga/${mangaId}/chapters/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to refresh manga chapters');
|
||||
}
|
||||
// L'endpoint retourne 202 (Accepted), pas de contenu JSON à parser
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async searchChapter(chapterId) {
|
||||
try {
|
||||
const response = await fetch('/api/scraping/chapters', {
|
||||
@@ -321,4 +341,40 @@ export class ApiMangaRepository {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async toggleMonitoring(mangaId, enabled) {
|
||||
try {
|
||||
const response = await fetch(`/api/manga/${mangaId}/monitoring/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ enabled })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Tenter de récupérer le message d'erreur détaillé de l'API
|
||||
let errorMessage = 'Failed to toggle monitoring';
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
if (errorData.detail) {
|
||||
errorMessage = errorData.detail;
|
||||
} else if (errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
} else if (errorData.violations && errorData.violations.length > 0) {
|
||||
errorMessage = errorData.violations.map(v => v.message).join(', ');
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('Could not parse error response:', parseError);
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// L'endpoint retourne un statut 204 (No Content), donc pas de données à retourner
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ref } from 'vue';
|
||||
import { useNotifications } from '../../../../shared/composables/useNotifications';
|
||||
import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
|
||||
|
||||
const mangaRepository = new ApiMangaRepository();
|
||||
|
||||
export function useMangaMonitoring() {
|
||||
const { showSuccess, showError } = useNotifications();
|
||||
|
||||
const isToggling = ref(false);
|
||||
const toggleError = ref(null);
|
||||
|
||||
const toggleMonitoring = async (mangaId, enabled) => {
|
||||
if (isToggling.value || !mangaId) return;
|
||||
|
||||
isToggling.value = true;
|
||||
toggleError.value = null;
|
||||
|
||||
try {
|
||||
console.log(`${enabled ? 'Activation' : 'Désactivation'} du monitoring pour le manga ${mangaId}`);
|
||||
|
||||
await mangaRepository.toggleMonitoring(mangaId, enabled);
|
||||
|
||||
const message = enabled
|
||||
? 'Monitoring activé avec succès. Vous recevrez les nouveaux chapitres automatiquement.'
|
||||
: 'Monitoring désactivé avec succès. Les nouveaux chapitres ne seront plus téléchargés automatiquement.';
|
||||
|
||||
showSuccess(message);
|
||||
|
||||
console.log(`Monitoring ${enabled ? 'activé' : 'désactivé'} avec succès`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du changement de monitoring:', error);
|
||||
toggleError.value = error.message || 'Erreur lors du changement de monitoring';
|
||||
|
||||
const errorMessage = enabled
|
||||
? `Erreur lors de l'activation du monitoring: ${error.message || 'Une erreur inattendue est survenue'}`
|
||||
: `Erreur lors de la désactivation du monitoring: ${error.message || 'Une erreur inattendue est survenue'}`;
|
||||
|
||||
showError(errorMessage);
|
||||
throw error;
|
||||
} finally {
|
||||
isToggling.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const enableMonitoring = async (mangaId) => {
|
||||
return await toggleMonitoring(mangaId, true);
|
||||
};
|
||||
|
||||
const disableMonitoring = async (mangaId) => {
|
||||
return await toggleMonitoring(mangaId, false);
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
toggleError.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
isToggling,
|
||||
toggleError,
|
||||
toggleMonitoring,
|
||||
enableMonitoring,
|
||||
disableMonitoring,
|
||||
clearError
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ref } from 'vue';
|
||||
import { useNotifications } from '../../../../shared/composables/useNotifications';
|
||||
import { useMangaStore } from '../../application/store/mangaStore';
|
||||
|
||||
export function useMangaRefresh() {
|
||||
const mangaStore = useMangaStore();
|
||||
const { showSuccess, showError } = useNotifications();
|
||||
|
||||
const isRefreshing = ref(false);
|
||||
const refreshError = ref(null);
|
||||
|
||||
const refreshMetadata = async (mangaId) => {
|
||||
if (isRefreshing.value || !mangaId) return;
|
||||
|
||||
isRefreshing.value = true;
|
||||
refreshError.value = null;
|
||||
|
||||
try {
|
||||
console.log(`Début du refresh des métadonnées pour le manga ${mangaId}`);
|
||||
|
||||
// Appel à l'endpoint de refresh des chapitres
|
||||
await mangaStore.refreshMangaChapters(mangaId);
|
||||
|
||||
showSuccess('Refresh des métadonnées lancé avec succès. Les nouveaux chapitres apparaîtront sous peu.');
|
||||
|
||||
console.log('Refresh des métadonnées déclenché avec succès');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du refresh des métadonnées:', error);
|
||||
refreshError.value = error.message || 'Erreur lors du refresh des métadonnées';
|
||||
showError(`Erreur lors du refresh: ${error.message || 'Une erreur inattendue est survenue'}`);
|
||||
throw error;
|
||||
} finally {
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
refreshError.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
isRefreshing,
|
||||
refreshError,
|
||||
refreshMetadata,
|
||||
clearError
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- Notifications Toast -->
|
||||
<NotificationToast />
|
||||
|
||||
<div v-if="errorDetails" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mx-4 mt-4">
|
||||
{{ errorDetails.message || 'Une erreur est survenue lors du chargement des détails.' }}
|
||||
</div>
|
||||
@@ -63,6 +66,7 @@
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
BookmarkIcon,
|
||||
BookmarkSlashIcon,
|
||||
ChevronDoubleDownIcon,
|
||||
Cog6ToothIcon,
|
||||
DocumentArrowDownIcon,
|
||||
@@ -75,7 +79,9 @@ import { useRoute } from 'vue-router';
|
||||
|
||||
import { useMangaDetails } from '../composables/useMangaDetails';
|
||||
import { useMangaEdit } from '../composables/useMangaEdit';
|
||||
import { useMangaMonitoring } from '../composables/useMangaMonitoring';
|
||||
import { useMangaPreferredSources } from '../composables/useMangaPreferredSources';
|
||||
import { useMangaRefresh } from '../composables/useMangaRefresh';
|
||||
import { useMangaVolumes } from '../composables/useMangaVolumes';
|
||||
|
||||
import MangaEditModal from '../components/MangaEditModal.vue';
|
||||
@@ -84,7 +90,8 @@ import MangaPreferredSourcesModal from '../components/MangaPreferredSourcesModal
|
||||
import MangaVolumeList from '../components/MangaVolumeList.vue';
|
||||
import MercureListener from '../components/MercureListener.vue';
|
||||
|
||||
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
||||
import NotificationToast from '../../../../shared/components/ui/NotificationToast.vue';
|
||||
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
||||
import { useMangaStore } from '../../application/store/mangaStore';
|
||||
|
||||
const route = useRoute();
|
||||
@@ -99,7 +106,8 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
data: currentManga,
|
||||
isLoading: isLoadingDetails,
|
||||
isFetching: isRefreshingDetails,
|
||||
error: errorDetails
|
||||
error: errorDetails,
|
||||
refetch: refetchMangaDetails
|
||||
} = useMangaDetails(mangaId);
|
||||
|
||||
const {
|
||||
@@ -127,6 +135,19 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
error: editError
|
||||
} = useMangaEdit();
|
||||
|
||||
// Composable pour le refresh des métadonnées
|
||||
const {
|
||||
isRefreshing,
|
||||
refreshMetadata
|
||||
} = useMangaRefresh();
|
||||
|
||||
// Composable pour le monitoring
|
||||
const {
|
||||
isToggling: isTogglingMonitoring,
|
||||
toggleMonitoring,
|
||||
toggleError: monitoringError
|
||||
} = useMangaMonitoring();
|
||||
|
||||
// Charger les chapitres dans le store quand le manga est chargé
|
||||
watch(
|
||||
mangaId,
|
||||
@@ -164,13 +185,42 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour le refresh des métadonnées
|
||||
const handleRefreshMetadata = async () => {
|
||||
if (!mangaId.value) return;
|
||||
|
||||
try {
|
||||
await refreshMetadata(mangaId.value);
|
||||
} catch (error) {
|
||||
// L'erreur est déjà gérée dans le composable avec les notifications
|
||||
console.error('Erreur lors du refresh:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour basculer le monitoring
|
||||
const handleToggleMonitoring = async () => {
|
||||
if (!mangaId.value || !currentManga.value) return;
|
||||
|
||||
try {
|
||||
const newMonitoringState = !currentManga.value.monitored;
|
||||
await toggleMonitoring(mangaId.value, newMonitoringState);
|
||||
|
||||
// Recharger les détails du manga pour mettre à jour l'état du monitoring
|
||||
await refetchMangaDetails();
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du changement de monitoring:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const toolbarConfig = computed(() => ({
|
||||
leftSection: [
|
||||
{
|
||||
icon: ArrowPathIcon,
|
||||
label: 'Refresh metadata',
|
||||
type: 'button',
|
||||
onClick: () => console.log('Refresh metadata')
|
||||
onClick: handleRefreshMetadata,
|
||||
loading: isRefreshing.value,
|
||||
disabled: isRefreshing.value
|
||||
},
|
||||
{
|
||||
icon: PencilSquareIcon,
|
||||
@@ -193,10 +243,13 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
],
|
||||
rightSection: [
|
||||
{
|
||||
icon: BookmarkIcon,
|
||||
label: 'Monitoring',
|
||||
icon: currentManga.value?.monitored ? BookmarkIcon : BookmarkSlashIcon,
|
||||
label: currentManga.value?.monitored ? 'Désactiver monitoring' : 'Activer monitoring',
|
||||
type: 'button',
|
||||
onClick: () => console.log('Monitoring')
|
||||
onClick: handleToggleMonitoring,
|
||||
loading: isTogglingMonitoring.value,
|
||||
disabled: isTogglingMonitoring.value,
|
||||
variant: currentManga.value?.monitored ? 'active' : 'default'
|
||||
},
|
||||
{
|
||||
icon: WrenchIcon,
|
||||
@@ -220,7 +273,7 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
}));
|
||||
|
||||
const loading = computed(() => isLoadingDetails.value || isLoadingVolumes.value);
|
||||
const isRefreshing = computed(() => isRefreshingDetails.value || isRefreshingVolumes.value);
|
||||
const isRefreshingData = computed(() => isRefreshingDetails.value || isRefreshingVolumes.value || isRefreshing.value);
|
||||
const error = computed(() => errorDetails.value || errorVolumes.value);
|
||||
|
||||
watch(
|
||||
|
||||
Reference in New Issue
Block a user