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,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>
|
||||
Reference in New Issue
Block a user