Files
Mangarr/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue

367 lines
12 KiB
Vue

<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>
<div v-else-if="currentManga" class="relative">
<!-- Composant invisible qui écoute les mises à jour Mercure -->
<MercureListener :manga-id="mangaId" />
<Toolbar :config="toolbarConfig" class="sticky top-16 z-10" />
<div v-if="isRefreshingDetails" class="absolute top-2 right-2 text-gray-500 z-20">
<ArrowPathIcon class="h-5 w-5 animate-spin" />
</div>
<MangaHeader :manga="currentManga" />
<!-- Section Volumes avec conteneur mobile -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 pb-8">
<div v-if="isLoadingVolumes" class="flex justify-center items-center h-32">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
<div v-else-if="errorVolumes" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{{ errorVolumes.message || 'Une erreur est survenue lors du chargement des volumes.' }}
</div>
<MangaVolumeList
v-else
ref="volumeListRef"
:volumes="volumes"
:manga-slug="currentManga.slug"
:manga-id="mangaId"
v-model:expand-all="isAllExpanded" />
</div>
<!-- Modale des sources préférées -->
<MangaPreferredSourcesModal
:is-open="isPreferredSourcesModalOpen"
:sources="preferredSources"
:is-loading="isLoadingSources"
:error="sourcesError"
:is-saving="isSavingSources"
@close="closePreferredSourcesModal"
@save="savePreferredSources"
/>
<!-- Modale d'édition du manga -->
<MangaEditModal
:is-open="isEditModalOpen"
:manga="currentManga"
:is-saving="isEditLoading"
:error="editError"
@close="closeEditModal"
@save="saveMangaEdit"
/>
<!-- Modale de gestion des chapitres -->
<ManageChaptersModal
:is-open="isManageChaptersModalOpen"
:manga="currentManga"
:chapters="mangaStore.mangaChapters[mangaId]?.items || []"
:is-loading="mangaStore.loadingChapters"
:is-saving="isSavingChapters"
:error="chaptersError"
@close="closeManageChaptersModal"
@save="saveChaptersChanges"
/>
</div>
<div v-else-if="isLoadingDetails" class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
<div v-else class="text-center text-gray-500 py-10 px-4">
Aucun manga sélectionné ou trouvé.
</div>
</div>
</template>
<script setup>
import {
ArrowPathIcon,
BookmarkIcon,
BookmarkSlashIcon,
ChevronDoubleDownIcon,
ChevronDoubleUpIcon,
Cog6ToothIcon,
PencilSquareIcon,
TrashIcon,
WrenchIcon
} from '@heroicons/vue/24/outline';
import { computed, onUnmounted, ref, watch } from 'vue';
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 ManageChaptersModal from '../components/ManageChaptersModal.vue';
import MangaEditModal from '../components/MangaEditModal.vue';
import MangaHeader from '../components/MangaHeader.vue';
import MangaPreferredSourcesModal from '../components/MangaPreferredSourcesModal.vue';
import MangaVolumeList from '../components/MangaVolumeList.vue';
import MercureListener from '../components/MercureListener.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();
const mangaStore = useMangaStore();
const mangaId = computed(() => route.params.id || null);
// État de la modale
const isPreferredSourcesModalOpen = ref(false);
const isManageChaptersModalOpen = ref(false);
const isSavingChapters = ref(false);
const chaptersError = ref(null);
// État d'expansion des volumes
const isAllExpanded = ref(false);
const volumeListRef = ref(null);
const {
data: currentManga,
isLoading: isLoadingDetails,
isFetching: isRefreshingDetails,
error: errorDetails,
refetch: refetchMangaDetails
} = useMangaDetails(mangaId);
const {
volumes,
isLoading: isLoadingVolumes,
isFetching: isRefreshingVolumes,
error: errorVolumes
} = useMangaVolumes(mangaId);
const {
sources: preferredSources,
isLoading: isLoadingSources,
error: sourcesError,
isSaving: isSavingSources,
savePreferredSources: saveSourcesOrder
} = useMangaPreferredSources(mangaId);
// Composable pour l'édition des mangas
const {
isEditModalOpen,
openEditModal,
closeEditModal,
editManga,
isLoading: isEditLoading,
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,
newId => {
if (newId) {
mangaStore.loadChapters(newId);
}
},
{ immediate: true }
);
const openPreferredSourcesModal = () => {
isPreferredSourcesModalOpen.value = true;
};
const closePreferredSourcesModal = () => {
isPreferredSourcesModalOpen.value = false;
};
const openManageChaptersModal = () => {
isManageChaptersModalOpen.value = true;
};
const closeManageChaptersModal = () => {
isManageChaptersModalOpen.value = false;
chaptersError.value = null;
};
const savePreferredSources = async (sourceIds) => {
try {
await saveSourcesOrder(sourceIds);
closePreferredSourcesModal();
} catch (error) {
console.error('Erreur lors de la sauvegarde des sources préférées:', error);
}
};
// Fonction pour sauvegarder l'édition du manga
const saveMangaEdit = async (updateData) => {
try {
await editManga(mangaId.value, updateData);
} catch (error) {
console.error('Erreur lors de l\'édition du manga:', error);
}
};
// Fonction pour sauvegarder les changements des chapitres
const saveChaptersChanges = async (chaptersData) => {
if (!mangaId.value) return;
isSavingChapters.value = true;
chaptersError.value = null;
try {
const response = await fetch('/api/chapters/batch-edit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chapters: chaptersData
})
});
if (!response.ok) {
throw new Error('Erreur lors de la sauvegarde des chapitres');
}
// Recharger les chapitres et les volumes
await mangaStore.loadChapters(mangaId.value);
closeManageChaptersModal();
} catch (error) {
chaptersError.value = error.message;
console.error('Erreur lors de la sauvegarde des chapitres:', error);
} finally {
isSavingChapters.value = false;
}
};
// 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);
}
};
// Fonction pour étendre/réduire tous les volumes
const handleExpandAll = () => {
if (!volumeListRef.value) return;
if (isAllExpanded.value) {
volumeListRef.value.collapseAllVolumes();
} else {
volumeListRef.value.expandAllVolumes();
}
};
const toolbarConfig = computed(() => ({
leftSection: [
{
icon: ArrowPathIcon,
label: 'Refresh metadata',
type: 'button',
onClick: handleRefreshMetadata,
loading: isRefreshing.value,
disabled: isRefreshing.value
},
{
icon: PencilSquareIcon,
label: 'Manage chapters',
type: 'button',
onClick: openManageChaptersModal
},
{
icon: Cog6ToothIcon,
label: 'Preferred Sources',
type: 'button',
onClick: openPreferredSourcesModal
}
],
rightSection: [
{
icon: currentManga.value?.monitored ? BookmarkIcon : BookmarkSlashIcon,
label: currentManga.value?.monitored ? 'Désactiver monitoring' : 'Activer monitoring',
type: 'button',
onClick: handleToggleMonitoring,
loading: isTogglingMonitoring.value,
disabled: isTogglingMonitoring.value,
variant: currentManga.value?.monitored ? 'active' : 'default'
},
{
icon: WrenchIcon,
label: 'Edit',
type: 'button',
onClick: openEditModal
},
{
icon: TrashIcon,
label: 'Delete',
type: 'button',
onClick: () => console.log('Delete')
},
{
icon: isAllExpanded.value ? ChevronDoubleUpIcon : ChevronDoubleDownIcon,
label: isAllExpanded.value ? 'Collapse all' : 'Expand all',
type: 'button',
onClick: handleExpandAll,
variant: isAllExpanded.value ? 'active' : 'default'
}
]
}));
const loading = computed(() => isLoadingDetails.value || isLoadingVolumes.value);
const isRefreshingData = computed(() => isRefreshingDetails.value || isRefreshingVolumes.value || isRefreshing.value);
const error = computed(() => errorDetails.value || errorVolumes.value);
watch(
mangaId,
newId => {
if (newId) {
mangaStore.setCurrentMangaId(newId);
}
},
{ immediate: true }
);
onUnmounted(() => {
mangaStore.clearCurrentMangaFocus();
});
</script>