feat: ajout des fonctionnalités de téléchargement et de masquage des chapitres, avec mise à jour des composants et de l'API pour gérer ces actions.

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-06-29 23:25:33 +02:00
parent 8692fa14c6
commit 17f9feea7b
8 changed files with 160 additions and 28 deletions

View File

@@ -209,6 +209,28 @@ export const useMangaStore = defineStore('manga', {
console.error('Erreur lors de la suppression du chapitre:', error);
throw error;
}
},
// --- Download Chapter Action ---
async downloadChapter(chapterId) {
try {
await mangaRepository.downloadChapter(chapterId);
} catch (error) {
console.error('Erreur lors du téléchargement du chapitre:', error);
throw error;
}
},
// --- Hide Chapter Action ---
async hideChapter(chapterId, mangaId) {
try {
await mangaRepository.hideChapter(chapterId);
// Recharger la liste des chapitres depuis l'API
await this.loadChapters(mangaId);
} catch (error) {
console.error('Erreur lors du masquage du chapitre:', error);
throw error;
}
}
}
});

View File

@@ -187,5 +187,60 @@ export class ApiMangaRepository {
console.error('API Error:', error);
throw error;
}
}
async downloadChapter(chapterId) {
try {
const response = await fetch(`/api/manga/chapters/${chapterId}/download`);
if (!response.ok) {
throw new Error('Failed to download chapter');
}
// Récupérer le nom du fichier depuis les headers
const contentDisposition = response.headers.get('Content-Disposition');
let filename = `chapter-${chapterId}.cbz`;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
// Créer un blob à partir de la réponse
const blob = await response.blob();
// Créer un lien de téléchargement temporaire
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
// Nettoyer
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
return true;
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
async hideChapter(chapterId) {
try {
const response = await fetch(`/api/manga/chapters/${chapterId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to hide chapter');
}
return true;
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
}

View File

@@ -23,10 +23,10 @@
<button v-else @click="handleDelete" class="text-gray-500 hover:text-green-500">
<XMarkIcon class="h-5 w-5" />
</button>
<button @click="handleDownload" class="text-gray-500 hover:text-green-500">
<button @click="handleDownload" :class="downloadButtonClass" :disabled="isDownloading || !chapter.isAvailable">
<ArrowDownTrayIcon class="h-5 w-5" />
</button>
<button @click="handleHide" class="text-gray-500 hover:text-green-500">
<button @click="handleHide" :class="hideButtonClass" :disabled="isHiding">
<TrashIcon class="h-5 w-5" />
</button>
</td>
@@ -46,16 +46,36 @@ import { useMangaStore } from '../../application/store/mangaStore';
mangaSlug: {
type: String,
required: true
},
mangaId: {
type: Number,
required: true
}
});
const store = useMangaStore();
const isLoading = ref(false);
const isDownloading = ref(false);
const isHiding = ref(false);
const buttonClass = computed(() => {
return isLoading.value ? 'text-yellow-500 cursor-wait' : 'text-gray-500 hover:text-green-500';
});
const downloadButtonClass = computed(() => {
if (isDownloading.value) {
return 'text-yellow-500 cursor-wait';
}
if (!props.chapter.isAvailable) {
return 'text-gray-300 cursor-not-allowed';
}
return 'text-gray-500 hover:text-green-500';
});
const hideButtonClass = computed(() => {
return isHiding.value ? 'text-yellow-500 cursor-wait' : 'text-gray-500 hover:text-green-500';
});
// Surveiller les changements d'état du chapitre
watch(
() => props.chapter.isAvailable,
@@ -96,12 +116,32 @@ import { useMangaStore } from '../../application/store/mangaStore';
};
const handleDownload = async () => {
// TODO: Implémenter le téléchargement du chapitre
console.log('Téléchargement du chapitre:', props.chapter.id);
try {
console.log(`MangaChapter: Téléchargement du chapitre ${props.chapter.number} (ID: ${props.chapter.id})`);
// Montrer l'indicateur de chargement
isDownloading.value = true;
await store.downloadChapter(props.chapter.id);
} catch (error) {
console.error('Erreur lors du téléchargement du chapitre:', error);
} finally {
// Arrêter l'indicateur de chargement
isDownloading.value = false;
}
};
const handleHide = async () => {
// TODO: Implémenter le masquage du chapitre
console.log('Masquage du chapitre:', props.chapter.id);
try {
console.log(`MangaChapter: Masquage du chapitre ${props.chapter.number} (ID: ${props.chapter.id})`);
// Montrer l'indicateur de chargement
isHiding.value = true;
await store.hideChapter(props.chapter.id, props.mangaId);
} catch (error) {
console.error('Erreur lors du masquage du chapitre:', error);
} finally {
// Arrêter l'indicateur de chargement
isHiding.value = false;
}
};
</script>

View File

@@ -13,7 +13,8 @@
v-for="chapter in chapters"
:key="chapter.id"
:chapter="chapter"
:manga-slug="mangaSlug" />
:manga-slug="mangaSlug"
:manga-id="mangaId" />
</tbody>
</table>
</div>
@@ -30,6 +31,10 @@
mangaSlug: {
type: String,
required: true
},
mangaId: {
type: Number,
required: true
}
});
</script>

View File

@@ -48,7 +48,7 @@
</div>
<!-- Liste des chapitres -->
<MangaChapterList v-show="isOpen" :chapters="volume.chapters" :manga-slug="mangaSlug" />
<MangaChapterList v-show="isOpen" :chapters="volume.chapters" :manga-slug="mangaSlug" :manga-id="mangaId" />
<!-- Chevron de fermeture -->
<div v-show="isOpen" class="flex justify-center p-2 py bg-white rounded-b-sm">
@@ -61,15 +61,15 @@
<script setup>
import {
ArrowDownTrayIcon,
BookmarkIcon,
ChevronDownIcon,
ChevronUpIcon,
MagnifyingGlassIcon
} from '@heroicons/vue/24/outline';
import { ref } from 'vue';
import { useMangaStore } from '../../application/store/mangaStore';
import MangaChapterList from './MangaChapterList.vue';
ArrowDownTrayIcon,
BookmarkIcon,
ChevronDownIcon,
ChevronUpIcon,
MagnifyingGlassIcon
} from '@heroicons/vue/24/outline';
import { ref } from 'vue';
import { useMangaStore } from '../../application/store/mangaStore';
import MangaChapterList from './MangaChapterList.vue';
const props = defineProps({
volume: {
@@ -80,6 +80,10 @@
type: String,
required: true
},
mangaId: {
type: Number,
required: true
},
isOpen: {
type: Boolean,
default: false

View File

@@ -5,6 +5,7 @@
:key="volume.number"
:volume="volume"
:mangaSlug="mangaSlug"
:mangaId="mangaId"
:isOpen="index === 0" />
</div>
</template>
@@ -20,6 +21,10 @@
mangaSlug: {
type: String,
required: true
},
mangaId: {
type: Number,
required: true
}
});
</script>

View File

@@ -22,7 +22,7 @@
<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" />
<MangaVolumeList v-else :volumes="volumes" :manga-slug="currentManga.slug" :manga-id="mangaId" />
</div>
<!-- Modale des sources préférées -->

View File

@@ -5,6 +5,8 @@ namespace App\Domain\Manga\Application\CommandHandler;
use App\Domain\Manga\Application\Command\DeleteChapter;
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Manga\Domain\Model\Chapter;
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
use App\Domain\Shared\Domain\Contract\CommandHandlerInterface;
use App\Domain\Shared\Domain\Contract\CommandInterface;
@@ -24,16 +26,15 @@ readonly class DeleteChapterHandler implements CommandHandlerInterface
throw new ChapterNotFoundException($command->chapterId);
}
// Soft delete by setting isVisible to false
$updatedChapter = new \App\Domain\Manga\Domain\Model\Chapter(
new \App\Domain\Manga\Domain\Model\ValueObject\ChapterId($chapter->getId()),
$chapter->getMangaId(),
$chapter->getNumber(),
$chapter->getTitle(),
$chapter->getVolume(),
false, // isVisible = false (soft delete)
$chapter->isAvailable(),
$chapter->getCreatedAt()
$updatedChapter = new Chapter(
id: new ChapterId($chapter->getId()),
mangaId: $chapter->getMangaId(),
number: $chapter->getNumber(),
title: $chapter->getTitle(),
volume: $chapter->getVolume(),
isVisible: false,
cbzPath: $chapter->getCbzPath(),
createdAt: $chapter->getCreatedAt()
);
$this->chapterRepository->save($updatedChapter);