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); console.error('Erreur lors de la suppression du chapitre:', error);
throw 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); console.error('API Error:', error);
throw 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"> <button v-else @click="handleDelete" class="text-gray-500 hover:text-green-500">
<XMarkIcon class="h-5 w-5" /> <XMarkIcon class="h-5 w-5" />
</button> </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" /> <ArrowDownTrayIcon class="h-5 w-5" />
</button> </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" /> <TrashIcon class="h-5 w-5" />
</button> </button>
</td> </td>
@@ -46,16 +46,36 @@ import { useMangaStore } from '../../application/store/mangaStore';
mangaSlug: { mangaSlug: {
type: String, type: String,
required: true required: true
},
mangaId: {
type: Number,
required: true
} }
}); });
const store = useMangaStore(); const store = useMangaStore();
const isLoading = ref(false); const isLoading = ref(false);
const isDownloading = ref(false);
const isHiding = ref(false);
const buttonClass = computed(() => { const buttonClass = computed(() => {
return isLoading.value ? 'text-yellow-500 cursor-wait' : 'text-gray-500 hover:text-green-500'; 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 // Surveiller les changements d'état du chapitre
watch( watch(
() => props.chapter.isAvailable, () => props.chapter.isAvailable,
@@ -96,12 +116,32 @@ import { useMangaStore } from '../../application/store/mangaStore';
}; };
const handleDownload = async () => { const handleDownload = async () => {
// TODO: Implémenter le téléchargement du chapitre try {
console.log('Téléchargement du chapitre:', props.chapter.id); 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 () => { const handleHide = async () => {
// TODO: Implémenter le masquage du chapitre try {
console.log('Masquage du chapitre:', props.chapter.id); 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> </script>

View File

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

View File

@@ -48,7 +48,7 @@
</div> </div>
<!-- Liste des chapitres --> <!-- 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 --> <!-- Chevron de fermeture -->
<div v-show="isOpen" class="flex justify-center p-2 py bg-white rounded-b-sm"> <div v-show="isOpen" class="flex justify-center p-2 py bg-white rounded-b-sm">
@@ -80,6 +80,10 @@
type: String, type: String,
required: true required: true
}, },
mangaId: {
type: Number,
required: true
},
isOpen: { isOpen: {
type: Boolean, type: Boolean,
default: false default: false

View File

@@ -5,6 +5,7 @@
:key="volume.number" :key="volume.number"
:volume="volume" :volume="volume"
:mangaSlug="mangaSlug" :mangaSlug="mangaSlug"
:mangaId="mangaId"
:isOpen="index === 0" /> :isOpen="index === 0" />
</div> </div>
</template> </template>
@@ -20,6 +21,10 @@
mangaSlug: { mangaSlug: {
type: String, type: String,
required: true required: true
},
mangaId: {
type: Number,
required: true
} }
}); });
</script> </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"> <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.' }} {{ errorVolumes.message || 'Une erreur est survenue lors du chargement des volumes.' }}
</div> </div>
<MangaVolumeList v-else :volumes="volumes" :manga-slug="currentManga.slug" /> <MangaVolumeList v-else :volumes="volumes" :manga-slug="currentManga.slug" :manga-id="mangaId" />
</div> </div>
<!-- Modale des sources préférées --> <!-- 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\Application\Command\DeleteChapter;
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; 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\CommandHandlerInterface;
use App\Domain\Shared\Domain\Contract\CommandInterface; use App\Domain\Shared\Domain\Contract\CommandInterface;
@@ -24,16 +26,15 @@ readonly class DeleteChapterHandler implements CommandHandlerInterface
throw new ChapterNotFoundException($command->chapterId); throw new ChapterNotFoundException($command->chapterId);
} }
// Soft delete by setting isVisible to false $updatedChapter = new Chapter(
$updatedChapter = new \App\Domain\Manga\Domain\Model\Chapter( id: new ChapterId($chapter->getId()),
new \App\Domain\Manga\Domain\Model\ValueObject\ChapterId($chapter->getId()), mangaId: $chapter->getMangaId(),
$chapter->getMangaId(), number: $chapter->getNumber(),
$chapter->getNumber(), title: $chapter->getTitle(),
$chapter->getTitle(), volume: $chapter->getVolume(),
$chapter->getVolume(), isVisible: false,
false, // isVisible = false (soft delete) cbzPath: $chapter->getCbzPath(),
$chapter->isAvailable(), createdAt: $chapter->getCreatedAt()
$chapter->getCreatedAt()
); );
$this->chapterRepository->save($updatedChapter); $this->chapterRepository->save($updatedChapter);