Initial commit: FuZip - Application de fusion interactive de fichiers ZIP

- 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
This commit is contained in:
2026-01-12 03:29:01 +01:00
commit bd6d321ed7
24 changed files with 6463 additions and 0 deletions

304
assets/js/UploadManager.js Normal file
View File

@@ -0,0 +1,304 @@
/**
* 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;