/** * 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}`); // column-header this.column = document.getElementById(`column-${side}`); this.dropZone = document.getElementById(`drop-zone-${side}`); this.fileInput = document.getElementById(`file-input-${side}`); this.btnBrowse = document.getElementById(`btn-browse-${side}`); this.panelInfo = document.getElementById(`panel-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}`); this.treePanel = document.getElementById(`tree-panel-${side}`); // État this.currentFile = null; this.structure = null; this.isUploading = false; this.init(); } /** * Initialise les événements */ init() { // Drag & Drop sur toute la colonne if (this.column) { this.column.addEventListener('dragover', (e) => this.onDragOver(e)); this.column.addEventListener('dragleave', (e) => this.onDragLeave(e)); this.column.addEventListener('drop', (e) => this.onDrop(e)); } // Click sur drop zone pour ouvrir file picker if (this.dropZone) { 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)); } // Bouton "Changer" const btnChange = document.getElementById(`btn-change-${this.side}`); if (btnChange) { btnChange.addEventListener('click', () => this.fileInput?.click()); } console.log(`[UploadManager] Initialized for side: ${this.side}`); } /** * Événement dragover */ onDragOver(e) { e.preventDefault(); e.stopPropagation(); this.column?.classList.add('drag-over'); } /** * Événement dragleave */ onDragLeave(e) { e.preventDefault(); e.stopPropagation(); // Vérifier qu'on quitte vraiment la colonne (pas juste un élément enfant) if (!this.column?.contains(e.relatedTarget)) { this.column?.classList.remove('drag-over'); } } /** * Événement drop */ onDrop(e) { e.preventDefault(); e.stopPropagation(); this.column?.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.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', 'compact'); this.column?.classList.add('success'); // Affiche le panel-info if (this.panelInfo) { this.panelInfo.classList.remove('hidden'); } 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(); } // Affiche le tree-panel if (this.treePanel) { this.treePanel.classList.remove('hidden'); } // 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.panelInfo) { this.panelInfo.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', 'compact'); this.column?.classList.remove('success', 'error'); this.panelInfo?.classList.add('hidden'); this.progressBar?.classList.add('hidden'); this.treePanel?.classList.add('hidden'); if (this.fileInput) { this.fileInput.value = ''; } } } // Export pour utilisation dans app.js window.UploadManager = UploadManager;