feat: page MangaDetails en vue.js
This commit is contained in:
parent
bee8572dc5
commit
41dc3c51aa
@@ -1,13 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-100">
|
<router-view></router-view>
|
||||||
<main>
|
|
||||||
<router-view v-slot="{ Component }">
|
|
||||||
<transition name="fade" mode="out-in">
|
|
||||||
<component :is="Component" />
|
|
||||||
</transition>
|
|
||||||
</router-view>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1,72 +1,89 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
|
||||||
import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
|
import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
|
||||||
|
|
||||||
const mangaRepository = new ApiMangaRepository();
|
const mangaRepository = new ApiMangaRepository();
|
||||||
|
|
||||||
export const useMangaStore = defineStore('manga', () => {
|
export const useMangaStore = defineStore('manga', {
|
||||||
const collection = ref(null);
|
state: () => ({
|
||||||
const detailedMangas = ref({});
|
// État pour la collection
|
||||||
const loading = ref(false);
|
collection: null,
|
||||||
const error = ref(null);
|
// État pour les détails
|
||||||
const isBackgroundLoading = ref(false);
|
currentManga: null,
|
||||||
|
chapters: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
isBackgroundLoading: false
|
||||||
|
}),
|
||||||
|
|
||||||
const loadCollection = async () => {
|
actions: {
|
||||||
if (loading.value) return;
|
// Actions pour la collection
|
||||||
|
async loadCollection() {
|
||||||
loading.value = true;
|
if (this.loading) return;
|
||||||
error.value = null;
|
|
||||||
|
console.log('Starting loadCollection...');
|
||||||
try {
|
this.loading = true;
|
||||||
collection.value = await mangaRepository.getCollection();
|
this.error = null;
|
||||||
} catch (err) {
|
|
||||||
error.value = err.message;
|
try {
|
||||||
console.error('Failed to load collection:', err);
|
console.log('Fetching collection from repository...');
|
||||||
} finally {
|
this.collection = await mangaRepository.getCollection();
|
||||||
loading.value = false;
|
console.log('Collection loaded:', this.collection);
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
console.error('Failed to load collection:', err);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
console.log('loadCollection finished. Loading:', this.loading);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshCollectionInBackground() {
|
||||||
|
if (this.isBackgroundLoading) return;
|
||||||
|
|
||||||
|
this.isBackgroundLoading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedCollection = await mangaRepository.getCollection();
|
||||||
|
this.collection = updatedCollection;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to refresh collection:', err);
|
||||||
|
} finally {
|
||||||
|
this.isBackgroundLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Actions pour les détails du manga
|
||||||
|
async fetchMangaDetails(mangaId) {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
try {
|
||||||
|
this.currentManga = await mangaRepository.getMangaById(mangaId);
|
||||||
|
} catch (error) {
|
||||||
|
this.error = error.message;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchMangaChapters(mangaId) {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
try {
|
||||||
|
const response = await mangaRepository.getChapters(mangaId);
|
||||||
|
console.log('API Response:', response); // Pour déboguer
|
||||||
|
this.chapters = Array.isArray(response) ? response :
|
||||||
|
(response.items ? response.items : []);
|
||||||
|
} catch (error) {
|
||||||
|
this.error = error.message;
|
||||||
|
console.error('Failed to fetch chapters:', error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearCurrentManga() {
|
||||||
|
this.currentManga = null;
|
||||||
|
this.chapters = [];
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const refreshCollectionInBackground = async () => {
|
|
||||||
if (isBackgroundLoading.value) return;
|
|
||||||
|
|
||||||
isBackgroundLoading.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updatedCollection = await mangaRepository.getCollection();
|
|
||||||
collection.value = updatedCollection;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to refresh collection:', err);
|
|
||||||
} finally {
|
|
||||||
isBackgroundLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadMangaDetail = async (slug) => {
|
|
||||||
if (detailedMangas.value[slug]) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const manga = await mangaRepository.getMangaBySlug(slug);
|
|
||||||
detailedMangas.value[slug] = manga;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Failed to load manga details for ${slug}:`, err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMangaFromCollection = (slug) => {
|
|
||||||
return collection.value?.items.find(manga => manga.slug === slug);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
collection,
|
|
||||||
detailedMangas,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
isBackgroundLoading,
|
|
||||||
loadCollection,
|
|
||||||
refreshCollectionInBackground,
|
|
||||||
loadMangaDetail,
|
|
||||||
getMangaFromCollection
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
17
assets/vue/app/domain/manga/infrastructure/api/MangaApi.js
Normal file
17
assets/vue/app/domain/manga/infrastructure/api/MangaApi.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export class MangaApi {
|
||||||
|
static async fetchById(mangaId) {
|
||||||
|
const response = await fetch(`/api/mangas/${mangaId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch manga details');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fetchChapters(mangaId) {
|
||||||
|
const response = await fetch(`/api/mangas/${mangaId}/chapters`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch manga chapters');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,32 @@ export class ApiMangaRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMangaById(id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/mangas/${id}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch manga details');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChapters(mangaId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/mangas/${mangaId}/chapters`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch manga chapters');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getMangaBySlug(slug) {
|
async getMangaBySlug(slug) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/mangas/${slug}`);
|
const response = await fetch(`/api/mangas/${slug}`);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="bg-white rounded-lg shadow-md overflow-hidden cursor-pointer transition-transform hover:scale-105"
|
class="bg-white rounded-lg shadow-md overflow-hidden cursor-pointer transition-transform hover:scale-105"
|
||||||
@click="handleClick"
|
@click="navigateToDetails"
|
||||||
>
|
>
|
||||||
<div class="relative pb-[150%]">
|
<div class="relative pb-[150%]">
|
||||||
<img
|
<img
|
||||||
@@ -34,8 +34,11 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClick = () => {
|
const navigateToDetails = () => {
|
||||||
router.push(`/manga/${props.manga.slug}`);
|
router.push({
|
||||||
|
name: 'manga-details',
|
||||||
|
params: { id: props.manga.id }
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Layout @add-manga-click="handleAddMangaClick">
|
<div>
|
||||||
<Toolbar :config="toolbarConfig" class="sticky top-16 z-10" />
|
<Toolbar :config="toolbarConfig" class="sticky top-16 z-10" />
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<MangaGrid :mangas="collection?.items || []" />
|
<MangaGrid :mangas="collection?.items || []" />
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
Mise à jour en cours...
|
Mise à jour en cours...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -15,7 +15,6 @@ import { onMounted } from 'vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useMangaStore } from '../../application/store/mangaStore';
|
import { useMangaStore } from '../../application/store/mangaStore';
|
||||||
import Layout from '../../../../shared/components/layout/Layout.vue';
|
|
||||||
import MangaGrid from '../components/MangaGrid.vue';
|
import MangaGrid from '../components/MangaGrid.vue';
|
||||||
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
||||||
import {
|
import {
|
||||||
@@ -37,10 +36,17 @@ const {
|
|||||||
isBackgroundLoading
|
isBackgroundLoading
|
||||||
} = storeToRefs(mangaStore);
|
} = storeToRefs(mangaStore);
|
||||||
|
|
||||||
const { loadCollection, refreshCollectionInBackground } = mangaStore;
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadCollection();
|
console.log('HomePage mounted');
|
||||||
|
console.log('Store state before loadCollection:', {
|
||||||
|
collection: collection.value,
|
||||||
|
loading: loading.value,
|
||||||
|
error: error.value
|
||||||
|
});
|
||||||
|
|
||||||
|
mangaStore.loadCollection();
|
||||||
|
|
||||||
|
console.log('loadCollection called');
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAddMangaClick = (query = '') => {
|
const handleAddMangaClick = (query = '') => {
|
||||||
@@ -52,7 +58,7 @@ const toolbarConfig = {
|
|||||||
{
|
{
|
||||||
icon: ArrowPathIcon,
|
icon: ArrowPathIcon,
|
||||||
label: 'Refresh',
|
label: 'Refresh',
|
||||||
onClick: refreshCollectionInBackground,
|
onClick: () => mangaStore.refreshCollectionInBackground(),
|
||||||
active: isBackgroundLoading
|
active: isBackgroundLoading
|
||||||
},
|
},
|
||||||
{ icon: MagnifyingGlassIcon, label: 'Search', onClick: () => {} }
|
{ icon: MagnifyingGlassIcon, label: 'Search', onClick: () => {} }
|
||||||
|
|||||||
279
assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue
Normal file
279
assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<template>
|
||||||
|
<!-- États de chargement et d'erreur -->
|
||||||
|
<div v-if="loading" 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-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="currentManga" class="relative">
|
||||||
|
<!-- Bannière avec image de fond -->
|
||||||
|
<div class="shadow-lg text-white">
|
||||||
|
<div class="relative h-96 bg-cover bg-center" :style="{ backgroundImage: `url('${currentManga.imageUrl}')` }">
|
||||||
|
<div class="absolute inset-0 bg-black opacity-50"></div>
|
||||||
|
<div class="absolute inset-0 flex flex-row justify-center p-4">
|
||||||
|
<!-- Image de couverture -->
|
||||||
|
<div class="hidden mr-12 xl:block 2xl:block">
|
||||||
|
<img :src="currentManga.thumbnailUrl" :alt="currentManga.title" class="max-w-72 max-h-72 ml-4">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informations du manga -->
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<BookmarkIcon class="h-8 w-8 text-white" />
|
||||||
|
<h1 class="text-3xl font-bold ml-4">{{ currentManga.title }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<span class="mr-4">{{ currentManga.year }}</span>
|
||||||
|
<span>Chapitres: {{ currentManga.totalChapters }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<FolderIcon class="h-6 w-6 text-gray-400 mr-2" />
|
||||||
|
<span class="truncate">/media/mangas/{{ currentManga.title }} ({{ currentManga.year }})</span>
|
||||||
|
<span class="ml-auto bg-green-600 py-1 px-2 rounded">{{ currentManga.status || 'Terminé' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<template v-if="currentManga.tags?.length">
|
||||||
|
<template v-for="(tag, index) in currentManga.tags.slice(0, 5)" :key="index">
|
||||||
|
<span class="bg-gray-700 py-1 px-2 rounded-sm mr-2">{{ tag }}</span>
|
||||||
|
</template>
|
||||||
|
<span v-if="currentManga.tags.length > 5" class="bg-gray-700 py-1 px-2 rounded-sm mr-2">...</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<HeartIcon class="h-6 w-6 text-red-500 mr-2" />
|
||||||
|
<span>{{ currentManga.rating }}</span>
|
||||||
|
</div>
|
||||||
|
<p>{{ currentManga.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Liste des volumes et chapitres -->
|
||||||
|
<div class="p-4">
|
||||||
|
<div v-for="volume in volumes" :key="volume.number" class="mb-4">
|
||||||
|
<div class="bg-white rounded-sm shadow">
|
||||||
|
<!-- En-tête du volume -->
|
||||||
|
<div class="relative flex items-center justify-between bg-white p-4 rounded-t-sm">
|
||||||
|
<!-- Partie gauche -->
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<BookmarkIcon class="h-8 w-8 text-gray-500" />
|
||||||
|
<h2 class="text-xl font-semibold w-28">Volume {{ String(volume.number).padStart(2, '0') }}</h2>
|
||||||
|
<div class="flex items-center w-16">
|
||||||
|
<span :class="[
|
||||||
|
'px-2 py-1 text-sm rounded w-full text-center text-white',
|
||||||
|
{
|
||||||
|
'bg-red-500': volume.progress === '0/0',
|
||||||
|
'bg-yellow-500': volume.progress !== volume.chapters.length + '/0',
|
||||||
|
'bg-green-500': volume.progress === volume.chapters.length + '/0'
|
||||||
|
}
|
||||||
|
]">
|
||||||
|
{{ volume.progress }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bouton toggle -->
|
||||||
|
<div class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||||
|
<component
|
||||||
|
:is="isVolumeOpen(volume.number) ? ChevronUpIcon : ChevronDownIcon"
|
||||||
|
class="h-6 w-6 bg-gray-400 rounded-full p-1 text-white hover:bg-green-500 cursor-pointer"
|
||||||
|
@click="toggleVolume(volume)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions du volume -->
|
||||||
|
<div class="flex space-x-2 text-xl text-bold">
|
||||||
|
<button class="w-8 text-center" @click="searchVolume(volume)">
|
||||||
|
<MagnifyingGlassIcon class="h-6 w-6 text-gray-500 hover:text-green-500" />
|
||||||
|
</button>
|
||||||
|
<button class="w-8 text-center" @click="downloadVolume(volume)">
|
||||||
|
<ArrowDownTrayIcon class="h-6 w-6 text-gray-500 hover:text-green-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Liste des chapitres -->
|
||||||
|
<div v-show="isVolumeOpen(volume.number)" class="p-4 border-t">
|
||||||
|
<table class="min-w-full table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2 text-left">#</th>
|
||||||
|
<th class="px-4 py-2 text-left">Titre</th>
|
||||||
|
<th class="px-4 py-2 text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="chapter in volume.chapters"
|
||||||
|
:key="chapter.id"
|
||||||
|
class="border-t hover:bg-green-100">
|
||||||
|
<td class="px-4 py-2" :class="{ 'text-green-500': chapter.isDownloaded }">
|
||||||
|
{{ String(chapter.number).padStart(2, '0') }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 w-full text-left">
|
||||||
|
<router-link
|
||||||
|
v-if="chapter.isDownloaded"
|
||||||
|
:to="{ name: 'reader', params: { mangaSlug: currentManga.slug, chapterNumber: chapter.number, pageNumber: 1 }}"
|
||||||
|
class="hover: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.isDownloaded"
|
||||||
|
@click="searchChapter(chapter)"
|
||||||
|
class="text-gray-500 hover:text-green-500">
|
||||||
|
<MagnifyingGlassIcon class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<button v-else
|
||||||
|
@click="deleteChapter(chapter)"
|
||||||
|
class="text-gray-500 hover:text-green-500">
|
||||||
|
<XMarkIcon class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<button @click="downloadChapter(chapter)"
|
||||||
|
class="text-gray-500 hover:text-green-500">
|
||||||
|
<ArrowDownTrayIcon class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<button @click="hideChapter(chapter)"
|
||||||
|
class="text-gray-500 hover:text-green-500">
|
||||||
|
<TrashIcon class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useMangaStore } from '../../application/store/mangaStore';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import {
|
||||||
|
BookmarkIcon,
|
||||||
|
FolderIcon,
|
||||||
|
HeartIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
ArrowDownTrayIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
TrashIcon
|
||||||
|
} from '@heroicons/vue/24/outline';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const store = useMangaStore();
|
||||||
|
const { currentManga, chapters, loading, error } = storeToRefs(store);
|
||||||
|
|
||||||
|
// Map réactif pour stocker l'état d'ouverture des volumes
|
||||||
|
const openVolumes = ref(new Map());
|
||||||
|
|
||||||
|
// Organise les chapitres par volumes
|
||||||
|
const volumes = computed(() => {
|
||||||
|
if (!chapters.value) return [];
|
||||||
|
|
||||||
|
const chaptersArray = Array.isArray(chapters.value) ? chapters.value :
|
||||||
|
(chapters.value.items ? chapters.value.items : []);
|
||||||
|
|
||||||
|
const volumeMap = new Map();
|
||||||
|
|
||||||
|
chaptersArray.forEach(chapter => {
|
||||||
|
const volumeNumber = chapter.volume || 'Unknown';
|
||||||
|
if (!volumeMap.has(volumeNumber)) {
|
||||||
|
// Initialiser l'état d'ouverture si ce n'est pas déjà fait
|
||||||
|
if (!openVolumes.value.has(volumeNumber)) {
|
||||||
|
openVolumes.value.set(volumeNumber, volumeNumber === chaptersArray[0]?.volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeMap.set(volumeNumber, {
|
||||||
|
number: volumeNumber,
|
||||||
|
progress: '0/0',
|
||||||
|
chapters: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
volumeMap.get(volumeNumber).chapters.push({
|
||||||
|
...chapter,
|
||||||
|
isDownloaded: Boolean(chapter.cbzPath)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calcul du progrès pour chaque volume
|
||||||
|
for (const volume of volumeMap.values()) {
|
||||||
|
const downloaded = volume.chapters.filter(c => c.isDownloaded).length;
|
||||||
|
const total = volume.chapters.length;
|
||||||
|
volume.progress = `${downloaded}/${total}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(volumeMap.values()).sort((a, b) => b.number - a.number);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isVolumeOpen = (volumeNumber) => {
|
||||||
|
return openVolumes.value.get(volumeNumber) || false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleVolume = (volume) => {
|
||||||
|
openVolumes.value.set(volume.number, !openVolumes.value.get(volume.number));
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
const mangaId = route.params.id;
|
||||||
|
await Promise.all([
|
||||||
|
store.fetchMangaDetails(mangaId),
|
||||||
|
store.fetchMangaChapters(mangaId)
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actions sur les chapitres et volumes
|
||||||
|
const searchChapter = async (chapter) => {
|
||||||
|
// TODO: Implémenter la recherche de chapitre
|
||||||
|
console.log('Recherche du chapitre:', chapter.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadChapter = async (chapter) => {
|
||||||
|
// TODO: Implémenter le téléchargement du chapitre
|
||||||
|
console.log('Téléchargement du chapitre:', chapter.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteChapter = async (chapter) => {
|
||||||
|
// TODO: Implémenter la suppression du chapitre
|
||||||
|
console.log('Suppression du chapitre:', chapter.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideChapter = async (chapter) => {
|
||||||
|
// TODO: Implémenter le masquage du chapitre
|
||||||
|
console.log('Masquage du chapitre:', chapter.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchVolume = async (volume) => {
|
||||||
|
// TODO: Implémenter la recherche du volume
|
||||||
|
console.log('Recherche du volume:', volume.number);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadVolume = async (volume) => {
|
||||||
|
// TODO: Implémenter le téléchargement du volume
|
||||||
|
console.log('Téléchargement du volume:', volume.number);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
store.clearCurrentManga();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,16 +1,9 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import routes from './router'
|
import { router } from './router'
|
||||||
import '../../styles/app.scss'
|
import '../../styles/app.scss'
|
||||||
|
|
||||||
// Création du router
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHistory('/vue/'),
|
|
||||||
routes
|
|
||||||
})
|
|
||||||
|
|
||||||
// Création du store
|
// Création du store
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
import Layout from '../shared/components/layout/Layout.vue';
|
||||||
import HomePage from '../domain/manga/presentation/pages/HomePage.vue';
|
import HomePage from '../domain/manga/presentation/pages/HomePage.vue';
|
||||||
|
|
||||||
// Placeholder component for new routes
|
// Placeholder component for new routes
|
||||||
@@ -18,110 +19,118 @@ const PlaceholderComponent = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomePage
|
component: HomePage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/manga/:slug',
|
path: '/manga/:id',
|
||||||
name: 'manga-detail',
|
name: 'manga-details',
|
||||||
component: PlaceholderComponent,
|
component: () => import('../domain/manga/presentation/pages/MangaDetails.vue')
|
||||||
props: { title: 'Détails du manga' }
|
},
|
||||||
},
|
{
|
||||||
{
|
|
||||||
path: '/add',
|
path: '/add',
|
||||||
name: 'add-manga',
|
name: 'add-manga',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Ajouter un manga' }
|
props: { title: 'Ajouter un manga' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/reader/:chapterId',
|
path: '/reader/:chapterId',
|
||||||
name: 'reader',
|
name: 'reader',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Lecteur' }
|
props: { title: 'Lecteur' }
|
||||||
},
|
},
|
||||||
// Pages placeholder avec chargement différé
|
// Pages placeholder avec chargement différé
|
||||||
{
|
{
|
||||||
path: '/import',
|
path: '/import',
|
||||||
name: 'import',
|
name: 'import',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Import de bibliothèque' }
|
props: { title: 'Import de bibliothèque' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/discover',
|
path: '/discover',
|
||||||
name: 'discover',
|
name: 'discover',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Découvrir' }
|
props: { title: 'Découvrir' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/convert',
|
path: '/convert',
|
||||||
name: 'convert',
|
name: 'convert',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Convertir CBR en CBZ' }
|
props: { title: 'Convertir CBR en CBZ' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/calendar',
|
path: '/calendar',
|
||||||
name: 'calendar',
|
name: 'calendar',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Calendrier' }
|
props: { title: 'Calendrier' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/activity',
|
path: '/activity',
|
||||||
name: 'activity',
|
name: 'activity',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Activité' }
|
props: { title: 'Activité' }
|
||||||
},
|
},
|
||||||
// Paramètres
|
// Paramètres
|
||||||
{
|
{
|
||||||
path: '/settings/general',
|
path: '/settings/general',
|
||||||
name: 'settings-general',
|
name: 'settings-general',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Paramètres généraux' }
|
props: { title: 'Paramètres généraux' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/folders',
|
path: '/settings/folders',
|
||||||
name: 'settings-folders',
|
name: 'settings-folders',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Gestion des dossiers' }
|
props: { title: 'Gestion des dossiers' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/scrappers',
|
path: '/settings/scrappers',
|
||||||
name: 'settings-scrappers',
|
name: 'settings-scrappers',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Configuration des scrappers' }
|
props: { title: 'Configuration des scrappers' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/ui',
|
path: '/settings/ui',
|
||||||
name: 'settings-ui',
|
name: 'settings-ui',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: "Paramètres de l'interface" }
|
props: { title: "Paramètres de l'interface" }
|
||||||
},
|
},
|
||||||
// Système
|
// Système
|
||||||
{
|
{
|
||||||
path: '/system/status',
|
path: '/system/status',
|
||||||
name: 'system-status',
|
name: 'system-status',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Status du système' }
|
props: { title: 'Status du système' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/system/backup',
|
path: '/system/backup',
|
||||||
name: 'system-backup',
|
name: 'system-backup',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Sauvegarde' }
|
props: { title: 'Sauvegarde' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/system/logs',
|
path: '/system/logs',
|
||||||
name: 'system-logs',
|
name: 'system-logs',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Journaux système' }
|
props: { title: 'Journaux système' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/system/updates',
|
path: '/system/updates',
|
||||||
name: 'system-updates',
|
name: 'system-updates',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Mises à jour' }
|
props: { title: 'Mises à jour' }
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export const router = createRouter({
|
||||||
|
history: createWebHistory('/vue/'),
|
||||||
|
routes
|
||||||
|
});
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<main class="pt-16 md:ml-60">
|
<main class="pt-16 md:ml-60">
|
||||||
<slot></slot>
|
<router-view></router-view>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user