Files
fuzip/assets/js/UploadManager.js
Charles afac7042a8 Refactoring interface web : optimisation layout colonnes et zone upload
- Fusion upload + tree en colonnes unifiées avec header unique
  - Déplacement bannière conflits en haut pour séparer les colonnes
  - Infos fichier condensées sur 1 ligne avec bouton icône à droite
  - Drop zone remplacée par tree après upload (gain d'espace ~60%)
  - Support drag & drop sur toute la colonne même avec fichier chargé
  - Styles optimisés : champ recherche intégré, bouton circulaire compact
2026-01-12 05:29:20 +01:00

329 lines
9.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}`); // 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;