feat: ajout de la fonctionnalité de téléchargement des volumes de manga, avec mise à jour de l'API et des composants pour gérer l'indicateur de chargement et le téléchargement des fichiers.

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-06-29 23:35:22 +02:00
parent 17f9feea7b
commit d23c82631e
4 changed files with 119 additions and 7 deletions

View File

@@ -231,6 +231,16 @@ export const useMangaStore = defineStore('manga', {
console.error('Erreur lors du masquage du chapitre:', error); console.error('Erreur lors du masquage du chapitre:', error);
throw error; throw error;
} }
},
// --- Download Volume Action ---
async downloadVolume(mangaId, volumeNumber) {
try {
await mangaRepository.downloadVolume(mangaId, volumeNumber);
} catch (error) {
console.error('Erreur lors du téléchargement du volume:', error);
throw error;
}
} }
} }
}); });

View File

@@ -243,4 +243,44 @@ export class ApiMangaRepository {
throw error; throw error;
} }
} }
async downloadVolume(mangaId, volumeNumber) {
try {
const response = await fetch(`/api/mangas/${mangaId}/volumes/${volumeNumber}/download`);
if (!response.ok) {
throw new Error('Failed to download volume');
}
// Récupérer le nom du fichier depuis les headers
const contentDisposition = response.headers.get('Content-Disposition');
let filename = `volume-${volumeNumber}.zip`;
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;
}
}
} }

View File

@@ -41,8 +41,15 @@
}"> }">
<MagnifyingGlassIcon class="h-6 w-6" /> <MagnifyingGlassIcon class="h-6 w-6" />
</button> </button>
<button class="w-8 text-center" @click="handleDownload"> <button
<ArrowDownTrayIcon class="h-6 w-6 text-gray-500 hover:text-green-500" /> class="w-8 text-center"
@click="handleDownload"
:class="{
'text-yellow-500 cursor-wait': isDownloading,
'text-gray-500 hover:text-green-500': !isDownloading
}"
:disabled="isDownloading">
<ArrowDownTrayIcon class="h-6 w-6" />
</button> </button>
</div> </div>
</div> </div>
@@ -93,6 +100,7 @@ import MangaChapterList from './MangaChapterList.vue';
const store = useMangaStore(); const store = useMangaStore();
const isOpen = ref(props.isOpen); const isOpen = ref(props.isOpen);
const isSearching = ref(false); const isSearching = ref(false);
const isDownloading = ref(false);
const toggleVolume = () => { const toggleVolume = () => {
isOpen.value = !isOpen.value; isOpen.value = !isOpen.value;
@@ -137,7 +145,17 @@ import MangaChapterList from './MangaChapterList.vue';
}; };
const handleDownload = async () => { const handleDownload = async () => {
// TODO: Implémenter le téléchargement du volume try {
console.log('Téléchargement du volume:', props.volume.number); console.log(`MangaVolume: Téléchargement du volume ${props.volume.number} (Manga ID: ${props.mangaId})`);
// Montrer l'indicateur de chargement
isDownloading.value = true;
await store.downloadVolume(props.mangaId, props.volume.number);
} catch (error) {
console.error('Erreur lors du téléchargement du volume:', error);
} finally {
// Arrêter l'indicateur de chargement
isDownloading.value = false;
}
}; };
</script> </script>

View File

@@ -39,11 +39,47 @@ readonly class FileService implements FileServiceInterface
throw new \RuntimeException('Cannot create CBZ file'); throw new \RuntimeException('Cannot create CBZ file');
} }
$imageCounter = 1;
foreach ($cbzPaths as $cbzPath) { foreach ($cbzPaths as $cbzPath) {
if ($this->cbzExists($cbzPath)) { if (!$this->cbzExists($cbzPath)) {
$filename = basename($cbzPath); continue;
$cbz->addFile($cbzPath, $filename);
} }
$sourceCbz = new \ZipArchive();
if ($sourceCbz->open($cbzPath) !== true) {
continue; // Skip if we can't open the CBZ
}
// Extract all images from the current CBZ
for ($i = 0; $i < $sourceCbz->numFiles; $i++) {
$fileName = $sourceCbz->getNameIndex($i);
$fileInfo = $sourceCbz->statIndex($i);
// Skip directories and non-image files
if ($fileInfo['size'] === 0 || !$this->isImageFile($fileName)) {
continue;
}
// Get the file content
$imageContent = $sourceCbz->getFromIndex($i);
if ($imageContent === false) {
continue;
}
// Get file extension
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
// Create a new filename with proper ordering
$newFileName = sprintf('%04d.%s', $imageCounter, $extension);
// Add the image to the volume CBZ
$cbz->addFromString($newFileName, $imageContent);
$imageCounter++;
}
$sourceCbz->close();
} }
$cbz->close(); $cbz->close();
@@ -61,6 +97,14 @@ readonly class FileService implements FileServiceInterface
return $response; return $response;
} }
private function isImageFile(string $fileName): bool
{
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
return in_array($extension, $imageExtensions);
}
public function deleteCbzFile(string $filePath): bool public function deleteCbzFile(string $filePath): bool
{ {
if (!$this->cbzExists($filePath)) { if (!$this->cbzExists($filePath)) {