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:
parent
8692fa14c6
commit
17f9feea7b
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user