feat: ajout de la fonctionnalité de conversion de fichiers CBR en CBZ, intégration d'un nouveau store pour gérer l'état de conversion, création de composants Vue pour l'upload de fichiers et le suivi de la progression, ainsi que la mise à jour de l'API pour gérer les conversions. Amélioration de la documentation API pour inclure les nouveaux endpoints et formats de fichiers supportés.
This commit is contained in:
parent
7a05934116
commit
d9e78b5229
@@ -0,0 +1,240 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ApiConversionRepository } from '../../infrastructure/api/apiConversionRepository';
|
||||
|
||||
const conversionRepository = new ApiConversionRepository();
|
||||
|
||||
export const useConversionStore = defineStore('conversion', {
|
||||
state: () => ({
|
||||
// État de conversion
|
||||
isConverting: false,
|
||||
conversionProgress: 0,
|
||||
conversionError: null,
|
||||
conversionSuccess: false,
|
||||
|
||||
// Fichier en cours de traitement
|
||||
currentFile: null,
|
||||
convertedFile: null,
|
||||
|
||||
// Historique des conversions (optionnel)
|
||||
conversionHistory: [],
|
||||
|
||||
// État de l'interface
|
||||
isDragOver: false,
|
||||
showSuccessMessage: false,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
/**
|
||||
* Indique si une conversion est en cours
|
||||
*/
|
||||
isProcessing: (state) => state.isConverting,
|
||||
|
||||
/**
|
||||
* Indique si un fichier est sélectionné
|
||||
*/
|
||||
hasSelectedFile: (state) => state.currentFile !== null,
|
||||
|
||||
/**
|
||||
* Indique si une conversion a réussi
|
||||
*/
|
||||
hasSucceeded: (state) => state.conversionSuccess && state.convertedFile !== null,
|
||||
|
||||
/**
|
||||
* Indique si une erreur est présente
|
||||
*/
|
||||
hasError: (state) => state.conversionError !== null,
|
||||
|
||||
/**
|
||||
* Obtient le nom du fichier actuel
|
||||
*/
|
||||
currentFileName: (state) => state.currentFile?.name || '',
|
||||
|
||||
/**
|
||||
* Obtient la taille formatée du fichier actuel
|
||||
*/
|
||||
currentFileSize: (state) => {
|
||||
if (!state.currentFile) return '';
|
||||
|
||||
const bytes = state.currentFile.size;
|
||||
const sizes = ['octets', 'Ko', 'Mo', 'Go'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtient le nombre de conversions réussies
|
||||
*/
|
||||
conversionCount: (state) => state.conversionHistory.length,
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Sélectionne un fichier pour la conversion
|
||||
* @param {File} file - Le fichier sélectionné
|
||||
*/
|
||||
selectFile(file) {
|
||||
// Validation du fichier
|
||||
const validation = conversionRepository.validateFile(file);
|
||||
|
||||
if (!validation.isValid) {
|
||||
this.setError(validation.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Réinitialisation de l'état
|
||||
this.clearError();
|
||||
this.conversionSuccess = false;
|
||||
this.convertedFile = null;
|
||||
this.showSuccessMessage = false;
|
||||
|
||||
// Stockage du fichier
|
||||
this.currentFile = file;
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lance la conversion du fichier sélectionné
|
||||
*/
|
||||
async convertCurrentFile() {
|
||||
if (!this.currentFile) {
|
||||
this.setError('Aucun fichier sélectionné');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.isConverting = true;
|
||||
this.conversionProgress = 0;
|
||||
this.clearError();
|
||||
|
||||
// Simulation du progrès (l'API ne fournit pas de progrès en temps réel)
|
||||
const progressInterval = setInterval(() => {
|
||||
if (this.conversionProgress < 90) {
|
||||
this.conversionProgress += Math.random() * 10;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Appel à l'API de conversion
|
||||
const convertedFileBlob = await conversionRepository.convertFile(this.currentFile);
|
||||
|
||||
// Nettoyage de l'interval de progrès
|
||||
clearInterval(progressInterval);
|
||||
this.conversionProgress = 100;
|
||||
|
||||
// Stockage du fichier converti
|
||||
this.convertedFile = convertedFileBlob;
|
||||
this.conversionSuccess = true;
|
||||
this.showSuccessMessage = true;
|
||||
|
||||
// Ajout à l'historique
|
||||
this.addToHistory({
|
||||
originalName: this.currentFile.name,
|
||||
originalSize: this.currentFile.size,
|
||||
convertedSize: convertedFileBlob.size,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.setError(error.message || 'Erreur lors de la conversion');
|
||||
return false;
|
||||
} finally {
|
||||
this.isConverting = false;
|
||||
this.conversionProgress = 0;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Télécharge le fichier converti
|
||||
*/
|
||||
downloadConvertedFile() {
|
||||
if (!this.convertedFile || !this.currentFile) {
|
||||
this.setError('Aucun fichier converti disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
conversionRepository.downloadConvertedFile(
|
||||
this.convertedFile,
|
||||
this.currentFile.name
|
||||
);
|
||||
} catch (error) {
|
||||
this.setError(error.message || 'Erreur lors du téléchargement');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Réinitialise l'état de conversion
|
||||
*/
|
||||
resetConversion() {
|
||||
this.currentFile = null;
|
||||
this.convertedFile = null;
|
||||
this.conversionSuccess = false;
|
||||
this.showSuccessMessage = false;
|
||||
this.conversionProgress = 0;
|
||||
this.clearError();
|
||||
},
|
||||
|
||||
/**
|
||||
* Définit une erreur
|
||||
* @param {string} message - Message d'erreur
|
||||
*/
|
||||
setError(message) {
|
||||
this.conversionError = message;
|
||||
this.conversionSuccess = false;
|
||||
this.showSuccessMessage = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Efface l'erreur actuelle
|
||||
*/
|
||||
clearError() {
|
||||
this.conversionError = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cache le message de succès
|
||||
*/
|
||||
hideSuccessMessage() {
|
||||
this.showSuccessMessage = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gère l'état du drag and drop
|
||||
* @param {boolean} isDragOver - Indique si un fichier est survolé
|
||||
*/
|
||||
setDragOver(isDragOver) {
|
||||
this.isDragOver = isDragOver;
|
||||
},
|
||||
|
||||
/**
|
||||
* Ajoute une conversion à l'historique
|
||||
* @param {Object} conversionData - Données de la conversion
|
||||
*/
|
||||
addToHistory(conversionData) {
|
||||
this.conversionHistory.unshift(conversionData);
|
||||
|
||||
// Limiter l'historique à 10 éléments
|
||||
if (this.conversionHistory.length > 10) {
|
||||
this.conversionHistory = this.conversionHistory.slice(0, 10);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Efface l'historique des conversions
|
||||
*/
|
||||
clearHistory() {
|
||||
this.conversionHistory = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Valide un fichier sans le sélectionner
|
||||
* @param {File} file - Le fichier à valider
|
||||
* @returns {Object} - Résultat de la validation
|
||||
*/
|
||||
validateFile(file) {
|
||||
return conversionRepository.validateFile(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
export class ApiConversionRepository {
|
||||
/**
|
||||
* Convertit un fichier CBR/CBZ en CBZ
|
||||
* @param {File} file - Le fichier à convertir
|
||||
* @returns {Promise<Blob>} - Le fichier converti
|
||||
*/
|
||||
async convertFile(file) {
|
||||
try {
|
||||
// Validation du fichier
|
||||
if (!file) {
|
||||
throw new Error('Aucun fichier fourni');
|
||||
}
|
||||
|
||||
// Validation de la taille (150MB max selon l'API)
|
||||
const maxSize = 150 * 1024 * 1024; // 150MB en bytes
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('Le fichier est trop volumineux (max 150MB)');
|
||||
}
|
||||
|
||||
// Validation du type de fichier
|
||||
const allowedTypes = ['.cbr', '.cbz'];
|
||||
const fileName = file.name.toLowerCase();
|
||||
const isValidType = allowedTypes.some(type => fileName.endsWith(type));
|
||||
|
||||
if (!isValidType) {
|
||||
throw new Error('Type de fichier non supporté. Seuls les fichiers .cbr et .cbz sont acceptés');
|
||||
}
|
||||
|
||||
// Création du FormData pour l'envoi multipart
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// Appel à l'API
|
||||
const response = await fetch('/api/conversions/convert', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
// On ne définit pas Content-Type pour laisser le navigateur gérer multipart/form-data
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Gestion des erreurs HTTP
|
||||
let errorMessage = 'Erreur lors de la conversion';
|
||||
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.message || errorData.detail || errorMessage;
|
||||
} catch {
|
||||
// Si la réponse n'est pas du JSON, on utilise le status text
|
||||
errorMessage = response.statusText || errorMessage;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Récupération du fichier converti
|
||||
const convertedFile = await response.blob();
|
||||
|
||||
// Vérification que le fichier n'est pas vide
|
||||
if (convertedFile.size === 0) {
|
||||
throw new Error('Le fichier converti est vide');
|
||||
}
|
||||
|
||||
return convertedFile;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la conversion:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Télécharge le fichier converti
|
||||
* @param {Blob} fileBlob - Le fichier à télécharger
|
||||
* @param {string} originalFileName - Nom original du fichier
|
||||
*/
|
||||
downloadConvertedFile(fileBlob, originalFileName) {
|
||||
try {
|
||||
// Génération du nom de fichier de sortie
|
||||
const baseName = originalFileName.replace(/\.(cbr|cbz)$/i, '');
|
||||
const outputFileName = `${baseName}.cbz`;
|
||||
|
||||
// Création d'un lien de téléchargement
|
||||
const url = URL.createObjectURL(fileBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = outputFileName;
|
||||
|
||||
// Déclenchement du téléchargement
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// Nettoyage
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du téléchargement:', error);
|
||||
throw new Error('Impossible de télécharger le fichier converti');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide si un fichier peut être converti
|
||||
* @param {File} file - Le fichier à valider
|
||||
* @returns {Object} - Résultat de la validation {isValid: boolean, error?: string}
|
||||
*/
|
||||
validateFile(file) {
|
||||
if (!file) {
|
||||
return { isValid: false, error: 'Aucun fichier sélectionné' };
|
||||
}
|
||||
|
||||
// Vérification de la taille
|
||||
const maxSize = 150 * 1024 * 1024; // 150MB
|
||||
if (file.size > maxSize) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Le fichier est trop volumineux (maximum 150MB)'
|
||||
};
|
||||
}
|
||||
|
||||
// Vérification du type
|
||||
const allowedTypes = ['.cbr', '.cbz'];
|
||||
const fileName = file.name.toLowerCase();
|
||||
const isValidType = allowedTypes.some(type => fileName.endsWith(type));
|
||||
|
||||
if (!isValidType) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Type de fichier non supporté. Seuls les fichiers .cbr et .cbz sont acceptés'
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Statut de la conversion -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- Icône de statut -->
|
||||
<div class="flex-shrink-0">
|
||||
<ArrowPathIcon
|
||||
v-if="isConverting"
|
||||
class="w-6 h-6 text-blue-500 animate-spin"
|
||||
/>
|
||||
<CheckCircleIcon
|
||||
v-else-if="isSuccess"
|
||||
class="w-6 h-6 text-green-500"
|
||||
/>
|
||||
<ExclamationTriangleIcon
|
||||
v-else-if="hasError"
|
||||
class="w-6 h-6 text-red-500"
|
||||
/>
|
||||
<ClockIcon
|
||||
v-else
|
||||
class="w-6 h-6 text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Message de statut -->
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
{{ statusMessage }}
|
||||
</p>
|
||||
<p v-if="fileName" class="text-xs text-gray-500">
|
||||
{{ fileName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Barre de progression -->
|
||||
<div v-if="showProgress" class="space-y-2">
|
||||
<div class="flex justify-between text-xs text-gray-600">
|
||||
<span>Progression</span>
|
||||
<span>{{ Math.round(progress) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-blue-500 h-2 rounded-full transition-all duration-300 ease-out"
|
||||
:style="{ width: `${progress}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Détails de la conversion -->
|
||||
<div v-if="showDetails && (originalSize || convertedSize)" class="text-xs text-gray-500 space-y-1">
|
||||
<div v-if="originalSize" class="flex justify-between">
|
||||
<span>Taille originale:</span>
|
||||
<span>{{ formatFileSize(originalSize) }}</span>
|
||||
</div>
|
||||
<div v-if="convertedSize" class="flex justify-between">
|
||||
<span>Taille convertie:</span>
|
||||
<span>{{ formatFileSize(convertedSize) }}</span>
|
||||
</div>
|
||||
<div v-if="originalSize && convertedSize" class="flex justify-between font-medium">
|
||||
<span>Gain d'espace:</span>
|
||||
<span :class="spaceSavingClass">{{ spaceSavingText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div v-if="showActions" class="flex space-x-3">
|
||||
<button
|
||||
v-if="canDownload"
|
||||
@click="$emit('download')"
|
||||
class="flex items-center space-x-2 px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
<ArrowDownTrayIcon class="w-4 h-4" />
|
||||
<span>Télécharger CBZ</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="canReset"
|
||||
@click="$emit('reset')"
|
||||
class="flex items-center space-x-2 px-4 py-2 border border-gray-300 text-gray-700 text-sm font-medium rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
<ArrowPathIcon class="w-4 h-4" />
|
||||
<span>Convertir un autre fichier</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Message d'erreur détaillé -->
|
||||
<div v-if="hasError && errorMessage" class="p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<div class="flex">
|
||||
<ExclamationTriangleIcon class="w-5 h-5 text-red-400 flex-shrink-0" />
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
Erreur de conversion
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-red-700">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ArrowPathIcon,
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
ExclamationTriangleIcon,
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'ConversionProgress',
|
||||
|
||||
components: {
|
||||
ArrowPathIcon,
|
||||
CheckCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
ClockIcon,
|
||||
ArrowDownTrayIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
isConverting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
progress: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
isSuccess: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
originalSize: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
convertedSize: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
showActions: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showDetails: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['download', 'reset'],
|
||||
|
||||
setup(props) {
|
||||
// Message de statut calculé
|
||||
const statusMessage = computed(() => {
|
||||
if (props.isConverting) {
|
||||
return 'Conversion en cours...';
|
||||
}
|
||||
if (props.isSuccess) {
|
||||
return 'Conversion terminée avec succès !';
|
||||
}
|
||||
if (props.hasError) {
|
||||
return 'Erreur lors de la conversion';
|
||||
}
|
||||
return 'En attente de fichier';
|
||||
});
|
||||
|
||||
// Affichage de la barre de progression
|
||||
const showProgress = computed(() => {
|
||||
return props.isConverting && props.progress > 0;
|
||||
});
|
||||
|
||||
// Actions disponibles
|
||||
const canDownload = computed(() => {
|
||||
return props.isSuccess && !props.isConverting;
|
||||
});
|
||||
|
||||
const canReset = computed(() => {
|
||||
return (props.isSuccess || props.hasError) && !props.isConverting;
|
||||
});
|
||||
|
||||
// Calcul du gain d'espace
|
||||
const spaceSaving = computed(() => {
|
||||
if (!props.originalSize || !props.convertedSize) return 0;
|
||||
return ((props.originalSize - props.convertedSize) / props.originalSize) * 100;
|
||||
});
|
||||
|
||||
const spaceSavingText = computed(() => {
|
||||
const saving = spaceSaving.value;
|
||||
if (saving > 0) {
|
||||
return `-${saving.toFixed(1)}%`;
|
||||
} else if (saving < 0) {
|
||||
return `+${Math.abs(saving).toFixed(1)}%`;
|
||||
}
|
||||
return '0%';
|
||||
});
|
||||
|
||||
const spaceSavingClass = computed(() => {
|
||||
const saving = spaceSaving.value;
|
||||
if (saving > 0) {
|
||||
return 'text-green-600';
|
||||
} else if (saving < 0) {
|
||||
return 'text-red-600';
|
||||
}
|
||||
return 'text-gray-600';
|
||||
});
|
||||
|
||||
// Formatage de la taille de fichier
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 octets';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['octets', 'Ko', 'Mo', 'Go'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
return {
|
||||
statusMessage,
|
||||
showProgress,
|
||||
canDownload,
|
||||
canReset,
|
||||
spaceSavingText,
|
||||
spaceSavingClass,
|
||||
formatFileSize,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragenter.prevent="handleDragEnter"
|
||||
@dragleave.prevent="handleDragLeave"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'border-2 border-dashed rounded-lg p-8 text-center transition-all duration-200',
|
||||
isDragOver
|
||||
? 'border-green-400 bg-green-50'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
]"
|
||||
>
|
||||
<!-- Zone d'upload -->
|
||||
<div class="space-y-4">
|
||||
<!-- Icône -->
|
||||
<div class="flex justify-center">
|
||||
<ArchiveBoxIcon
|
||||
:class="[
|
||||
'w-16 h-16 transition-colors duration-200',
|
||||
isDragOver ? 'text-green-500' : 'text-gray-400'
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Message principal -->
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
{{ isDragOver ? 'Déposez votre fichier ici' : 'Sélectionnez un fichier CBR ou CBZ' }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500">
|
||||
Glissez-déposez votre fichier ou cliquez pour le sélectionner
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
Fichiers supportés: .cbr, .cbz (max. 150MB)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Bouton de sélection -->
|
||||
<div class="flex justify-center">
|
||||
<label
|
||||
for="file-upload"
|
||||
:class="[
|
||||
'relative cursor-pointer rounded-md px-4 py-2 font-medium text-white transition-colors duration-200',
|
||||
isDragOver
|
||||
? 'bg-green-500 hover:bg-green-600'
|
||||
: 'bg-green-600 hover:bg-green-700'
|
||||
]"
|
||||
>
|
||||
<span>Sélectionner un fichier</span>
|
||||
<input
|
||||
id="file-upload"
|
||||
name="file-upload"
|
||||
type="file"
|
||||
class="sr-only"
|
||||
accept=".cbr,.cbz"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Informations du fichier sélectionné -->
|
||||
<div v-if="selectedFile" class="mt-6 p-4 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center space-x-3">
|
||||
<DocumentIcon class="w-8 h-8 text-gray-600" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate">
|
||||
{{ selectedFile.name }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
{{ formatFileSize(selectedFile.size) }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="clearFile"
|
||||
class="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
title="Supprimer le fichier"
|
||||
>
|
||||
<XMarkIcon class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overlay pendant le drag -->
|
||||
<div
|
||||
v-if="isDragOver"
|
||||
class="absolute inset-0 bg-green-100 bg-opacity-50 rounded-lg flex items-center justify-center"
|
||||
style="pointer-events: none;"
|
||||
>
|
||||
<div class="text-green-600 font-medium text-lg">
|
||||
Déposez le fichier ici
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArchiveBoxIcon, DocumentIcon, XMarkIcon } from '@heroicons/vue/24/outline';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'FileUploadArea',
|
||||
|
||||
components: {
|
||||
ArchiveBoxIcon,
|
||||
DocumentIcon,
|
||||
XMarkIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
selectedFile: {
|
||||
type: File,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['file-selected', 'file-cleared'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const isDragOver = ref(false);
|
||||
const dragCounter = ref(0);
|
||||
|
||||
// Handlers pour le drag & drop
|
||||
const handleDragEnter = (event) => {
|
||||
if (props.disabled) return;
|
||||
|
||||
event.preventDefault();
|
||||
dragCounter.value++;
|
||||
isDragOver.value = true;
|
||||
};
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
if (props.disabled) return;
|
||||
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleDragLeave = (event) => {
|
||||
if (props.disabled) return;
|
||||
|
||||
event.preventDefault();
|
||||
dragCounter.value--;
|
||||
|
||||
if (dragCounter.value === 0) {
|
||||
isDragOver.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (event) => {
|
||||
if (props.disabled) return;
|
||||
|
||||
event.preventDefault();
|
||||
isDragOver.value = false;
|
||||
dragCounter.value = 0;
|
||||
|
||||
const files = event.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
emit('file-selected', file);
|
||||
}
|
||||
};
|
||||
|
||||
// Handler pour la sélection de fichier via input
|
||||
const handleFileSelect = (event) => {
|
||||
if (props.disabled) return;
|
||||
|
||||
const files = event.target.files;
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
emit('file-selected', file);
|
||||
}
|
||||
|
||||
// Réinitialiser l'input pour permettre la sélection du même fichier
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
// Supprimer le fichier sélectionné
|
||||
const clearFile = () => {
|
||||
emit('file-cleared');
|
||||
};
|
||||
|
||||
// Formater la taille du fichier
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 octets';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['octets', 'Ko', 'Mo', 'Go'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
return {
|
||||
isDragOver,
|
||||
handleDragEnter,
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
handleDrop,
|
||||
handleFileSelect,
|
||||
clearFile,
|
||||
formatFileSize,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<!-- En-tête -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center space-x-3 mb-4">
|
||||
<ArrowPathIcon class="w-8 h-8 text-green-600" />
|
||||
<h1 class="text-3xl font-bold text-gray-900">
|
||||
Convertir CBR en CBZ
|
||||
</h1>
|
||||
</div>
|
||||
<p class="text-lg text-gray-600">
|
||||
Convertissez vos fichiers CBR (Comic Book RAR) en CBZ (Comic Book ZIP) pour une meilleure compatibilité.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Zone principale -->
|
||||
<div class="bg-white shadow-lg rounded-lg overflow-hidden">
|
||||
<!-- En-tête de la carte -->
|
||||
<div class="bg-gray-800 text-white p-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<ArchiveBoxIcon class="w-6 h-6" />
|
||||
<h2 class="text-xl font-semibold">
|
||||
Conversion de fichiers
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenu de la carte -->
|
||||
<div class="p-6 space-y-6">
|
||||
<!-- Zone d'upload -->
|
||||
<FileUploadArea
|
||||
:selected-file="conversionStore.currentFile"
|
||||
:disabled="conversionStore.isProcessing"
|
||||
@file-selected="handleFileSelected"
|
||||
@file-cleared="handleFileClear"
|
||||
/>
|
||||
|
||||
<!-- Bouton de conversion -->
|
||||
<div v-if="conversionStore.hasSelectedFile && !conversionStore.hasSucceeded" class="flex justify-center">
|
||||
<button
|
||||
@click="handleConvert"
|
||||
:disabled="conversionStore.isProcessing"
|
||||
:class="[
|
||||
'flex items-center space-x-2 px-6 py-3 text-white font-medium rounded-lg transition-all duration-200',
|
||||
conversionStore.isProcessing
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2'
|
||||
]"
|
||||
>
|
||||
<ArrowPathIcon
|
||||
:class="[
|
||||
'w-5 h-5',
|
||||
conversionStore.isProcessing && 'animate-spin'
|
||||
]"
|
||||
/>
|
||||
<span>
|
||||
{{ conversionStore.isProcessing ? 'Conversion en cours...' : 'Convertir en CBZ' }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Progression et résultat -->
|
||||
<ConversionProgress
|
||||
v-if="showProgress"
|
||||
:is-converting="conversionStore.isProcessing"
|
||||
:progress="conversionStore.conversionProgress"
|
||||
:is-success="conversionStore.hasSucceeded"
|
||||
:has-error="conversionStore.hasError"
|
||||
:error-message="conversionStore.conversionError"
|
||||
:file-name="conversionStore.currentFileName"
|
||||
:original-size="conversionStore.currentFile?.size || 0"
|
||||
:converted-size="conversionStore.convertedFile?.size || 0"
|
||||
@download="handleDownload"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
|
||||
<!-- Message d'information -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex">
|
||||
<InformationCircleIcon class="w-5 h-5 text-blue-500 flex-shrink-0" />
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">
|
||||
À propos de la conversion
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-blue-700 space-y-1">
|
||||
<p>• Les fichiers CBZ sont plus largement supportés par les lecteurs de bandes dessinées</p>
|
||||
<p>• La compression ZIP permet généralement une meilleure accessibilité</p>
|
||||
<p>• Aucune perte de qualité lors de la conversion</p>
|
||||
<p>• Taille maximale supportée: 150MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historique des conversions -->
|
||||
<div v-if="conversionStore.conversionCount > 0" class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
Historique des conversions
|
||||
</h3>
|
||||
<button
|
||||
@click="handleClearHistory"
|
||||
class="text-sm text-gray-500 hover:text-gray-700 transition-colors"
|
||||
>
|
||||
Effacer l'historique
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="(conversion, index) in conversionStore.conversionHistory"
|
||||
:key="index"
|
||||
class="flex items-center justify-between py-2 border-b border-gray-200 last:border-b-0"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
{{ conversion.originalName }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
{{ formatDate(conversion.timestamp) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ formatFileSize(conversion.originalSize) }} → {{ formatFileSize(conversion.convertedSize) }}
|
||||
</p>
|
||||
<p class="text-xs text-green-600">
|
||||
{{ calculateSaving(conversion.originalSize, conversion.convertedSize) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast de notification -->
|
||||
<div
|
||||
v-if="conversionStore.showSuccessMessage"
|
||||
class="fixed bottom-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-3 z-50"
|
||||
>
|
||||
<CheckCircleIcon class="w-5 h-5" />
|
||||
<span class="font-medium">Conversion réussie !</span>
|
||||
<button
|
||||
@click="conversionStore.hideSuccessMessage()"
|
||||
class="ml-2 text-green-100 hover:text-white transition-colors"
|
||||
>
|
||||
<XMarkIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
ArchiveBoxIcon,
|
||||
ArrowPathIcon,
|
||||
CheckCircleIcon,
|
||||
InformationCircleIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useConversionStore } from '../../application/store/conversionStore';
|
||||
import ConversionProgress from '../components/ConversionProgress.vue';
|
||||
import FileUploadArea from '../components/FileUploadArea.vue';
|
||||
|
||||
export default {
|
||||
name: 'ConversionPage',
|
||||
|
||||
components: {
|
||||
FileUploadArea,
|
||||
ConversionProgress,
|
||||
ArrowPathIcon,
|
||||
ArchiveBoxIcon,
|
||||
InformationCircleIcon,
|
||||
CheckCircleIcon,
|
||||
XMarkIcon,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const conversionStore = useConversionStore();
|
||||
|
||||
// Computed properties
|
||||
const showProgress = computed(() => {
|
||||
return conversionStore.hasSelectedFile &&
|
||||
(conversionStore.isProcessing || conversionStore.hasSucceeded || conversionStore.hasError);
|
||||
});
|
||||
|
||||
// Event handlers
|
||||
const handleFileSelected = (file) => {
|
||||
const success = conversionStore.selectFile(file);
|
||||
if (!success) {
|
||||
// L'erreur est déjà gérée par le store
|
||||
console.warn('Fichier non valide:', file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileClear = () => {
|
||||
conversionStore.resetConversion();
|
||||
};
|
||||
|
||||
const handleConvert = async () => {
|
||||
if (!conversionStore.currentFile) return;
|
||||
|
||||
const success = await conversionStore.convertCurrentFile();
|
||||
if (success) {
|
||||
console.log('Conversion réussie');
|
||||
} else {
|
||||
console.error('Échec de la conversion');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
conversionStore.downloadConvertedFile();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
conversionStore.resetConversion();
|
||||
};
|
||||
|
||||
const handleClearHistory = () => {
|
||||
conversionStore.clearHistory();
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 octets';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['octets', 'Ko', 'Mo', 'Go'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
const formatDate = (isoString) => {
|
||||
const date = new Date(isoString);
|
||||
return new Intl.DateTimeFormat('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const calculateSaving = (originalSize, convertedSize) => {
|
||||
if (!originalSize || !convertedSize) return '';
|
||||
|
||||
const saving = ((originalSize - convertedSize) / originalSize) * 100;
|
||||
if (saving > 0) {
|
||||
return `-${saving.toFixed(1)}%`;
|
||||
} else if (saving < 0) {
|
||||
return `+${Math.abs(saving).toFixed(1)}%`;
|
||||
}
|
||||
return '0%';
|
||||
};
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
// Réinitialiser l'état au montage de la page
|
||||
conversionStore.resetConversion();
|
||||
});
|
||||
|
||||
return {
|
||||
conversionStore,
|
||||
showProgress,
|
||||
handleFileSelected,
|
||||
handleFileClear,
|
||||
handleConvert,
|
||||
handleDownload,
|
||||
handleReset,
|
||||
handleClearHistory,
|
||||
formatFileSize,
|
||||
formatDate,
|
||||
calculateSaving,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Styles spécifiques si nécessaires */
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import ActivityPage from '../domain/activity/presentation/pages/ActivityPage.vue';
|
||||
import ConversionPage from '../domain/conversion/presentation/pages/ConversionPage.vue';
|
||||
import AddManga from '../domain/manga/presentation/pages/AddManga.vue';
|
||||
import HomePage from '../domain/manga/presentation/pages/HomePage.vue';
|
||||
import MangaDetails from '../domain/manga/presentation/pages/MangaDetails.vue';
|
||||
@@ -71,8 +72,7 @@ const routes = [
|
||||
{
|
||||
path: '/convert',
|
||||
name: 'convert',
|
||||
component: PlaceholderComponent,
|
||||
props: { title: 'Convertir CBR en CBZ' }
|
||||
component: ConversionPage
|
||||
},
|
||||
{
|
||||
path: '/calendar',
|
||||
|
||||
@@ -12,7 +12,7 @@ use Arkitect\Rules\Rule;
|
||||
|
||||
return static function (Config $config): void {
|
||||
$domainClassSet = ClassSet::fromDir(__DIR__ . '/src/Domain');
|
||||
$businessDomains = ['Manga', 'Reader', 'Scraping'];
|
||||
$businessDomains = ['Manga', 'Reader', 'Scraping', 'Conversion'];
|
||||
|
||||
// Classes PHP standards et utilitaires
|
||||
$standardExceptions = [
|
||||
@@ -64,7 +64,7 @@ return static function (Config $config): void {
|
||||
->that(new ResideInOneOfTheseNamespaces("App\Domain\\$domain\Application"))
|
||||
->should(new NotHaveDependencyOutsideNamespace(
|
||||
"App\Domain\\$domain",
|
||||
array_merge($standardExceptions, $externalDependencies, ['App\Domain\Shared\Contract'])
|
||||
array_merge($standardExceptions, $externalDependencies, ['App\Domain\Shared\Domain\Contract'])
|
||||
))
|
||||
->because("la couche Application de $domain ne peut dépendre que de son propre domaine, des contrats partagés et des dépendances autorisées");
|
||||
|
||||
|
||||
@@ -205,6 +205,22 @@
|
||||
"_embedded"
|
||||
]
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,6 +274,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {}
|
||||
@@ -294,6 +320,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
@@ -331,6 +367,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -374,6 +420,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {}
|
||||
@@ -410,6 +466,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
@@ -447,6 +513,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -502,6 +578,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {}
|
||||
@@ -556,6 +642,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentSource"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
@@ -564,6 +660,53 @@
|
||||
},
|
||||
"parameters": []
|
||||
},
|
||||
"/api/conversions/convert": {
|
||||
"post": {
|
||||
"operationId": "api_conversionsconvert_post",
|
||||
"tags": [
|
||||
"Conversion"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "File converted successfully",
|
||||
"content": {
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Convert comic book file to CBZ",
|
||||
"description": "Converts a CBR or CBZ file to CBZ format and returns the converted file for download",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"file"
|
||||
],
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
"description": "Comic book file to convert (CBR, CBZ, max 150MB)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
"deprecated": false
|
||||
},
|
||||
"parameters": []
|
||||
},
|
||||
"/api/jobs": {
|
||||
"get": {
|
||||
"operationId": "api_jobs_get_collection",
|
||||
@@ -757,6 +900,22 @@
|
||||
"_embedded"
|
||||
]
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Job"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Job"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1056,6 +1215,12 @@
|
||||
},
|
||||
"application/hal+json": {
|
||||
"schema": {}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1114,6 +1279,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Mangadex.MangaSearchCollection.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Mangadex.MangaSearchCollection"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Mangadex.MangaSearchCollection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1336,6 +1511,22 @@
|
||||
"_embedded"
|
||||
]
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Manga.MangaCollection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Manga.MangaCollection"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1392,6 +1583,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga.MangaDetail.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga.MangaDetail"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga.MangaDetail"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1450,6 +1651,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga.MangaDetail.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga.MangaDetail"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga.MangaDetail"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1508,6 +1719,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {}
|
||||
@@ -1544,6 +1765,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
@@ -1581,6 +1812,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Mangadex.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Mangadex"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Mangadex"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {}
|
||||
@@ -1733,6 +1974,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Chapters.ChapterCollection.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Chapters.ChapterCollection"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Chapters.ChapterCollection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1840,6 +2091,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {}
|
||||
@@ -1894,6 +2155,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Manga"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
@@ -2022,6 +2293,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {}
|
||||
@@ -2101,6 +2382,12 @@
|
||||
},
|
||||
"application/hal+json": {
|
||||
"schema": {}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2461,6 +2748,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {}
|
||||
@@ -2497,6 +2794,16 @@
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping.jsonhal"
|
||||
}
|
||||
},
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping"
|
||||
}
|
||||
},
|
||||
"application/x-cbz": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
@@ -3297,6 +3604,154 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Conversion": {
|
||||
"type": "object",
|
||||
"description": "",
|
||||
"deprecated": false,
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "binary"
|
||||
},
|
||||
"fileName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"fileContent": {},
|
||||
"filename": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"originalConvertedFilePath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Conversion.jsonhal": {
|
||||
"type": "object",
|
||||
"description": "",
|
||||
"deprecated": false,
|
||||
"properties": {
|
||||
"_links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"self": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"href": {
|
||||
"type": "string",
|
||||
"format": "iri-reference"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"file": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "binary"
|
||||
},
|
||||
"fileName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"fileContent": {},
|
||||
"filename": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"originalConvertedFilePath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Conversion.jsonld": {
|
||||
"type": "object",
|
||||
"description": "",
|
||||
"deprecated": false,
|
||||
"properties": {
|
||||
"@context": {
|
||||
"readOnly": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@vocab": {
|
||||
"type": "string"
|
||||
},
|
||||
"hydra": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"http://www.w3.org/ns/hydra/core#"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"@vocab",
|
||||
"hydra"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"@id": {
|
||||
"readOnly": true,
|
||||
"type": "string"
|
||||
},
|
||||
"@type": {
|
||||
"readOnly": true,
|
||||
"type": "string"
|
||||
},
|
||||
"file": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "binary"
|
||||
},
|
||||
"fileName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"fileContent": {},
|
||||
"filename": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"originalConvertedFilePath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Job": {
|
||||
"type": "object",
|
||||
"description": "Liste des jobs",
|
||||
|
||||
21
symfony.lock
21
symfony.lock
@@ -25,6 +25,15 @@
|
||||
"config/packages/dama_doctrine_test_bundle.yaml"
|
||||
]
|
||||
},
|
||||
"doctrine/deprecations": {
|
||||
"version": "1.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
|
||||
}
|
||||
},
|
||||
"doctrine/doctrine-bundle": {
|
||||
"version": "2.11",
|
||||
"recipe": {
|
||||
@@ -350,6 +359,18 @@
|
||||
"twig/extra-bundle": {
|
||||
"version": "v3.10.0"
|
||||
},
|
||||
"vich/uploader-bundle": {
|
||||
"version": "2.7",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "1.13",
|
||||
"ref": "1b3064c2f6b255c2bc2f56461aaeb76b11e07e36"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/vich_uploader.yaml"
|
||||
]
|
||||
},
|
||||
"zenstruck/foundry": {
|
||||
"version": "1.36",
|
||||
"recipe": {
|
||||
|
||||
Reference in New Issue
Block a user