- Backend PHP: architecture MVC avec API REST (upload, merge, preview, extract) - Frontend JavaScript: composants modulaires (arborescence, upload, themes, i18n) - Fonctionnalités: drag&drop, sélection exclusive, détection conflits, persistance état - Sécurité: validation stricte, isolation sessions, sanitization chemins - UI/UX: responsive, thèmes clair/sombre, multi-langue (FR/EN) - Documentation: README complet avec installation et utilisation
305 lines
8.8 KiB
JavaScript
305 lines
8.8 KiB
JavaScript
/**
|
|
* FuZip - Gestionnaire d'upload
|
|
* Gère le drag & drop, browse, upload vers API, progression
|
|
*/
|
|
|
|
class UploadManager {
|
|
constructor(side, onStructureLoaded) {
|
|
this.side = side; // 'left' ou 'right'
|
|
this.onStructureLoaded = onStructureLoaded; // Callback quand structure reçue
|
|
this.apiBase = window.FUZIP_CONFIG?.apiBase || '/api/';
|
|
this.i18n = window.FUZIP_CONFIG?.i18n || {};
|
|
|
|
// Éléments DOM
|
|
this.panel = document.getElementById(`upload-panel-${side}`);
|
|
this.dropZone = document.getElementById(`drop-zone-${side}`);
|
|
this.fileInput = document.getElementById(`file-input-${side}`);
|
|
this.btnBrowse = document.getElementById(`btn-browse-${side}`);
|
|
this.uploadInfo = document.getElementById(`upload-info-${side}`);
|
|
this.fileName = document.getElementById(`file-name-${side}`);
|
|
this.fileSize = document.getElementById(`file-size-${side}`);
|
|
this.fileCount = document.getElementById(`file-count-${side}`);
|
|
this.progressBar = document.getElementById(`progress-bar-${side}`);
|
|
this.progressFill = document.getElementById(`progress-fill-${side}`);
|
|
this.progressText = document.getElementById(`progress-text-${side}`);
|
|
|
|
// État
|
|
this.currentFile = null;
|
|
this.structure = null;
|
|
this.isUploading = false;
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialise les événements
|
|
*/
|
|
init() {
|
|
// Drag & Drop
|
|
if (this.dropZone) {
|
|
this.dropZone.addEventListener('dragover', (e) => this.onDragOver(e));
|
|
this.dropZone.addEventListener('dragleave', (e) => this.onDragLeave(e));
|
|
this.dropZone.addEventListener('drop', (e) => this.onDrop(e));
|
|
this.dropZone.addEventListener('click', () => this.fileInput?.click());
|
|
}
|
|
|
|
// Browse button
|
|
if (this.btnBrowse) {
|
|
this.btnBrowse.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this.fileInput?.click();
|
|
});
|
|
}
|
|
|
|
// File input change
|
|
if (this.fileInput) {
|
|
this.fileInput.addEventListener('change', (e) => this.onFileSelected(e));
|
|
}
|
|
|
|
console.log(`[UploadManager] Initialized for side: ${this.side}`);
|
|
}
|
|
|
|
/**
|
|
* Événement dragover
|
|
*/
|
|
onDragOver(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.dropZone?.classList.add('drag-over');
|
|
}
|
|
|
|
/**
|
|
* Événement dragleave
|
|
*/
|
|
onDragLeave(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.dropZone?.classList.remove('drag-over');
|
|
}
|
|
|
|
/**
|
|
* Événement drop
|
|
*/
|
|
onDrop(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.dropZone?.classList.remove('drag-over');
|
|
|
|
const files = e.dataTransfer?.files;
|
|
if (files && files.length > 0) {
|
|
this.handleFile(files[0]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Événement file input change
|
|
*/
|
|
onFileSelected(e) {
|
|
const files = e.target?.files;
|
|
if (files && files.length > 0) {
|
|
this.handleFile(files[0]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gère un fichier sélectionné
|
|
* @param {File} file
|
|
*/
|
|
async handleFile(file) {
|
|
// Validation basique
|
|
if (!file.name.endsWith('.zip')) {
|
|
this.showError(this.i18n.error_not_zip || 'Le fichier doit être un ZIP');
|
|
return;
|
|
}
|
|
|
|
if (file.size > 500 * 1024 * 1024) { // 500 MB
|
|
this.showError(this.i18n.error_file_too_large || 'Fichier trop volumineux (max 500 MB)');
|
|
return;
|
|
}
|
|
|
|
this.currentFile = file;
|
|
|
|
// Upload
|
|
await this.upload();
|
|
}
|
|
|
|
/**
|
|
* Upload le fichier vers l'API
|
|
*/
|
|
async upload() {
|
|
if (!this.currentFile || this.isUploading) return;
|
|
|
|
this.isUploading = true;
|
|
this.panel?.classList.add('uploading');
|
|
this.showProgress(0);
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', this.currentFile);
|
|
formData.append('side', this.side);
|
|
|
|
try {
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
// Progression
|
|
xhr.upload.addEventListener('progress', (e) => {
|
|
if (e.lengthComputable) {
|
|
const percent = Math.round((e.loaded / e.total) * 100);
|
|
this.showProgress(percent);
|
|
}
|
|
});
|
|
|
|
// Promesse pour gérer le résultat
|
|
const response = await new Promise((resolve, reject) => {
|
|
xhr.addEventListener('load', () => {
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
try {
|
|
const data = JSON.parse(xhr.responseText);
|
|
resolve(data);
|
|
} catch (err) {
|
|
reject(new Error('Réponse JSON invalide'));
|
|
}
|
|
} else {
|
|
reject(new Error(`Erreur HTTP ${xhr.status}`));
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('error', () => reject(new Error('Erreur réseau')));
|
|
xhr.addEventListener('abort', () => reject(new Error('Upload annulé')));
|
|
|
|
xhr.open('POST', `${this.apiBase}upload.php`);
|
|
xhr.send(formData);
|
|
});
|
|
|
|
// Succès
|
|
if (response.success) {
|
|
this.structure = response.structure;
|
|
this.showSuccess(response.stats);
|
|
|
|
// Callback vers app.js
|
|
if (this.onStructureLoaded) {
|
|
this.onStructureLoaded(this.side, response.structure, response.stats);
|
|
}
|
|
} else {
|
|
throw new Error(response.error || 'Erreur inconnue');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`[UploadManager] Upload error for ${this.side}:`, error);
|
|
this.showError(error.message);
|
|
} finally {
|
|
this.isUploading = false;
|
|
this.panel?.classList.remove('uploading');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Affiche la progression
|
|
* @param {number} percent - 0-100
|
|
*/
|
|
showProgress(percent) {
|
|
if (this.uploadInfo) {
|
|
this.uploadInfo.classList.remove('hidden');
|
|
}
|
|
|
|
if (this.progressBar) {
|
|
this.progressBar.classList.remove('hidden');
|
|
}
|
|
|
|
if (this.progressFill) {
|
|
this.progressFill.style.width = `${percent}%`;
|
|
}
|
|
|
|
if (this.progressText) {
|
|
this.progressText.textContent = `${percent}%`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Affiche le succès avec stats
|
|
* @param {Object} stats
|
|
*/
|
|
showSuccess(stats) {
|
|
this.panel?.classList.remove('error');
|
|
this.panel?.classList.add('success');
|
|
|
|
if (this.fileName) {
|
|
this.fileName.textContent = this.currentFile?.name || '';
|
|
}
|
|
|
|
if (this.fileSize) {
|
|
this.fileSize.textContent = this.formatBytes(stats.total_size || 0);
|
|
}
|
|
|
|
if (this.fileCount) {
|
|
this.fileCount.textContent = (stats.total_files || 0).toString();
|
|
}
|
|
|
|
// Cache la progress bar après 1s
|
|
setTimeout(() => {
|
|
this.progressBar?.classList.add('hidden');
|
|
}, 1000);
|
|
}
|
|
|
|
/**
|
|
* Affiche une erreur
|
|
* @param {string} message
|
|
*/
|
|
showError(message) {
|
|
this.panel?.classList.remove('success');
|
|
this.panel?.classList.add('error');
|
|
|
|
alert(message); // TODO: Afficher dans l'UI plutôt qu'une alerte
|
|
|
|
// Réinitialise
|
|
this.currentFile = null;
|
|
this.structure = null;
|
|
if (this.uploadInfo) {
|
|
this.uploadInfo.classList.add('hidden');
|
|
}
|
|
if (this.progressBar) {
|
|
this.progressBar.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formate les octets en format lisible
|
|
* @param {number} bytes
|
|
* @returns {string}
|
|
*/
|
|
formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
/**
|
|
* Récupère la structure actuelle
|
|
* @returns {Object|null}
|
|
*/
|
|
getStructure() {
|
|
return this.structure;
|
|
}
|
|
|
|
/**
|
|
* Réinitialise l'upload
|
|
*/
|
|
reset() {
|
|
this.currentFile = null;
|
|
this.structure = null;
|
|
this.isUploading = false;
|
|
|
|
this.panel?.classList.remove('success', 'error', 'uploading');
|
|
this.uploadInfo?.classList.add('hidden');
|
|
this.progressBar?.classList.add('hidden');
|
|
|
|
if (this.fileInput) {
|
|
this.fileInput.value = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export pour utilisation dans app.js
|
|
window.UploadManager = UploadManager;
|