- 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
329 lines
9.8 KiB
JavaScript
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;
|