351 lines
12 KiB
Vue
351 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 :volumes="volumes" :manga-slug="currentManga.slug" :manga-id="mangaId" />
|
|
</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,
|
|
Cog6ToothIcon,
|
|
DocumentArrowDownIcon,
|
|
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);
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
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: DocumentArrowDownIcon,
|
|
label: 'Manage cbz',
|
|
type: 'button',
|
|
onClick: () => console.log('Manage cbz')
|
|
},
|
|
{
|
|
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: ChevronDoubleDownIcon,
|
|
label: 'Expand all',
|
|
type: 'button',
|
|
onClick: () => console.log('Expand all')
|
|
}
|
|
]
|
|
}));
|
|
|
|
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>
|