286 lines
9.1 KiB
Vue
286 lines
9.1 KiB
Vue
<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>
|