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