/** * FuZip - Application principale * Point d'entrée et orchestrateur de l'application */ class FuZipApp { constructor() { // Vérification config if (!window.FUZIP_CONFIG) { console.error('[FuZipApp] FUZIP_CONFIG not found!'); return; } // État de l'application this.state = { leftStructure: null, rightStructure: null, leftStats: null, rightStats: null, conflicts: [], selection: new Map() // path -> 'left'|'right'|null }; // Managers this.themeManager = null; this.langManager = null; this.uploadLeft = null; this.uploadRight = null; this.treeRendererLeft = null; this.treeRendererRight = null; this.previewManager = null; // Éléments DOM this.conflictsBanner = document.getElementById('conflicts-banner'); this.conflictsCount = document.getElementById('conflicts-count'); this.btnMerge = document.getElementById('btn-merge'); this.btnReset = document.getElementById('btn-reset'); this.selectionCount = document.getElementById('selection-count'); this.loadingOverlay = document.getElementById('loading-overlay'); this.loadingText = document.getElementById('loading-text'); this.treeContainerLeft = document.getElementById('tree-left'); this.treeContainerRight = document.getElementById('tree-right'); this.init(); } /** * Initialise l'application */ async init() { console.log('[FuZipApp] Initializing...'); // Initialise les managers de base this.themeManager = new ThemeManager(); this.langManager = new LanguageManager(); // Initialise les gestionnaires d'upload this.uploadLeft = new UploadManager('left', (side, structure, stats) => { this.onStructureLoaded(side, structure, stats); }); this.uploadRight = new UploadManager('right', (side, structure, stats) => { this.onStructureLoaded(side, structure, stats); }); // Initialise les renderers d'arbres (Phase 6) if (this.treeContainerLeft) { this.treeRendererLeft = new FileTreeRenderer('left', this.treeContainerLeft, this); } if (this.treeContainerRight) { this.treeRendererRight = new FileTreeRenderer('right', this.treeContainerRight, this); } // Initialise le gestionnaire de preview (Phase 6) this.previewManager = new PreviewManager(); // Événements boutons this.setupEventListeners(); // Nettoyage automatique des anciennes sessions this.cleanupOldSessions(); // Restaurer l'état sauvegardé si disponible this.restoreState(); console.log('[FuZipApp] Initialized successfully'); } /** * Configure les événements globaux */ setupEventListeners() { // Bouton merge if (this.btnMerge) { this.btnMerge.addEventListener('click', () => this.onMergeClick()); this.updateMergeButton(); // Désactive par défaut } // Bouton reset if (this.btnReset) { this.btnReset.addEventListener('click', () => this.reset()); } // Cleanup au unload window.addEventListener('beforeunload', () => { // Placeholder pour cleanup si nécessaire }); } /** * Affiche les informations d'upload dans l'UI * @param {string} side - 'left' ou 'right' * @param {Object} stats - {total_files, total_size} * @param {string|null} fileNameText - Nom du fichier à afficher (optionnel) */ displayUploadInfo(side, stats, fileNameText = null) { const uploadInfo = document.getElementById(`upload-info-${side}`); const fileName = document.getElementById(`file-name-${side}`); const fileSize = document.getElementById(`file-size-${side}`); const fileCount = document.getElementById(`file-count-${side}`); const panel = document.getElementById(`upload-panel-${side}`); if (panel) { panel.classList.add('success'); } if (uploadInfo) { uploadInfo.classList.remove('hidden'); } if (fileName && fileNameText) { fileName.textContent = fileNameText; } if (fileCount && stats.total_files !== undefined) { fileCount.textContent = stats.total_files.toString(); } if (fileSize && stats.total_size !== undefined) { fileSize.textContent = this.formatBytes(stats.total_size); } } /** * 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]; } /** * Callback quand une structure est chargée * @param {string} side - 'left' ou 'right' * @param {Object} structure * @param {Object} stats */ async onStructureLoaded(side, structure, stats) { console.log(`[FuZipApp] Structure loaded for ${side}:`, stats); // Mise à jour de l'état if (side === 'left') { this.state.leftStructure = structure; this.state.leftStats = stats; } else { this.state.rightStructure = structure; this.state.rightStats = stats; } // Rend l'arborescence (Phase 6) const renderer = side === 'left' ? this.treeRendererLeft : this.treeRendererRight; if (renderer) { // Attendre les conflits si les deux sont chargés if (this.state.leftStructure && this.state.rightStructure) { await this.detectConflicts(); } // Rendre avec les conflits const conflictPaths = this.state.conflicts.map(c => c.path); renderer.render(structure, conflictPaths); } // Met à jour le bouton merge this.updateMergeButton(); // Sauvegarder l'état this.saveState(); } /** * Détecte les conflits entre les deux structures */ async detectConflicts() { try { const response = await fetch(`${window.FUZIP_CONFIG.apiBase}structure.php?action=conflicts`); const data = await response.json(); if (data.success && data.conflicts) { this.state.conflicts = data.conflicts; this.showConflictsBanner(data.conflicts.length); } } catch (error) { console.error('[FuZipApp] Error detecting conflicts:', error); } } /** * Affiche la bannière de conflits * @param {number} count */ showConflictsBanner(count) { if (count > 0 && this.conflictsBanner) { this.conflictsBanner.classList.remove('hidden'); if (this.conflictsCount) { this.conflictsCount.textContent = count.toString(); } } else if (this.conflictsBanner) { this.conflictsBanner.classList.add('hidden'); } } /** * Met à jour l'état du bouton merge */ updateMergeButton() { if (!this.btnMerge) return; // Active seulement si les deux structures sont chargées const canMerge = this.state.leftStructure && this.state.rightStructure; this.btnMerge.disabled = !canMerge; } /** * Optimise la sélection en supprimant les enfants si le parent est sélectionné * @param {Object} selection - Sélection {path: 'left'|'right'|null} * @returns {Object} Sélection optimisée */ optimizeSelection(selection) { const optimized = {}; for (const [path, side] of Object.entries(selection)) { if (side === null) { continue; } // Vérifier si un parent de ce chemin est déjà sélectionné du même côté let hasParentSelected = false; for (const [otherPath, otherSide] of Object.entries(selection)) { if (otherSide === side && otherPath !== path) { // Vérifier si otherPath est un parent de path // Un parent se termine par / et path commence par ce parent if (otherPath.endsWith('/') && path.startsWith(otherPath)) { hasParentSelected = true; break; } } } // Si aucun parent n'est sélectionné, garder ce chemin if (!hasParentSelected) { optimized[path] = side; } } return optimized; } /** * Callback quand la sélection change */ onSelectionChanged() { // Compte les fichiers sélectionnés let count = 0; this.state.selection.forEach((side, path) => { if (side !== null) { count++; } }); // Met à jour le footer if (this.selectionCount) { this.selectionCount.textContent = count.toString(); } // Met à jour les deux renderers if (this.treeRendererLeft) { this.treeRendererLeft.updateAllCheckboxStates(); } if (this.treeRendererRight) { this.treeRendererRight.updateAllCheckboxStates(); } // Sauvegarder l'état this.saveState(); } /** * Événement clic sur le bouton merge */ async onMergeClick() { if (!this.state.leftStructure || !this.state.rightStructure) { return; } this.showLoading('Fusion en cours...'); try { // Convertit la sélection Map en objet pour JSON const selection = {}; this.state.selection.forEach((side, path) => { selection[path] = side; }); // Optimiser la sélection (supprimer les enfants si parent déjà sélectionné) const optimizedSelection = this.optimizeSelection(selection); console.log(`[FuZipApp] Selection: ${Object.keys(selection).length} paths -> ${Object.keys(optimizedSelection).length} paths (optimized)`); const response = await fetch(`${window.FUZIP_CONFIG.apiBase}merge.php`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ selection: optimizedSelection }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } // Le serveur envoie le fichier ZIP en stream const blob = await response.blob(); const url = window.URL.createObjectURL(blob); // Déclenche le téléchargement const a = document.createElement('a'); a.href = url; a.download = 'merged.zip'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); console.log('[FuZipApp] Merge completed successfully'); } catch (error) { console.error('[FuZipApp] Merge error:', error); alert(`Erreur lors de la fusion : ${error.message}`); } finally { this.hideLoading(); } } /** * Affiche l'overlay de loading * @param {string} text */ showLoading(text = 'Chargement...') { if (this.loadingOverlay) { this.loadingOverlay.classList.remove('hidden'); } if (this.loadingText) { this.loadingText.textContent = text; } } /** * Cache l'overlay de loading */ hideLoading() { if (this.loadingOverlay) { this.loadingOverlay.classList.add('hidden'); } } /** * Nettoyage automatique des anciennes sessions */ async cleanupOldSessions() { try { await fetch(`${window.FUZIP_CONFIG.apiBase}cleanup.php`); console.log('[FuZipApp] Old sessions cleaned'); } catch (error) { console.warn('[FuZipApp] Cleanup failed:', error); } } /** * Sauvegarde l'état de l'application dans localStorage */ saveState() { try { // Récupérer les noms de fichiers affichés const fileNameLeft = document.getElementById('file-name-left'); const fileNameRight = document.getElementById('file-name-right'); const state = { sessionId: window.FUZIP_CONFIG.sessionId, leftStructure: this.state.leftStructure, rightStructure: this.state.rightStructure, leftStats: this.state.leftStats, rightStats: this.state.rightStats, leftFileName: fileNameLeft ? fileNameLeft.textContent : null, rightFileName: fileNameRight ? fileNameRight.textContent : null, conflicts: this.state.conflicts, selection: Array.from(this.state.selection.entries()), expandedFoldersLeft: this.treeRendererLeft ? Array.from(this.treeRendererLeft.expandedFolders) : [], expandedFoldersRight: this.treeRendererRight ? Array.from(this.treeRendererRight.expandedFolders) : [], searchQueryLeft: this.treeRendererLeft ? this.treeRendererLeft.searchQuery : '', searchQueryRight: this.treeRendererRight ? this.treeRendererRight.searchQuery : '' }; localStorage.setItem('fuzip_state', JSON.stringify(state)); console.log('[FuZipApp] State saved'); } catch (error) { console.warn('[FuZipApp] Failed to save state:', error); } } /** * Restaure l'état de l'application depuis localStorage */ async restoreState() { try { const saved = localStorage.getItem('fuzip_state'); if (!saved) { console.log('[FuZipApp] No saved state found'); return; } const state = JSON.parse(saved); // Vérifier que la session PHP est toujours la même if (state.sessionId !== window.FUZIP_CONFIG.sessionId) { console.log('[FuZipApp] Session ID mismatch, clearing old state'); localStorage.removeItem('fuzip_state'); return; } // Vérifier que les structures existent if (!state.leftStructure && !state.rightStructure) { console.log('[FuZipApp] No structures to restore'); return; } console.log('[FuZipApp] Restoring state...'); // Restaurer conflicts EN PREMIER (utilisés par render) if (state.conflicts) { this.state.conflicts = state.conflicts; this.showConflictsBanner(state.conflicts.length); } // Restaurer selection EN SECOND (utilisée par updateAllCheckboxStates) if (state.selection) { this.state.selection = new Map(state.selection); // Ne pas appeler onSelectionChanged() maintenant, on le fera après les renders } // Restaurer left if (state.leftStructure) { this.state.leftStructure = state.leftStructure; this.state.leftStats = state.leftStats; // Afficher les stats dans l'UI this.displayUploadInfo('left', state.leftStats, state.leftFileName); // Restaurer expanded folders AVANT le rendu if (this.treeRendererLeft && state.expandedFoldersLeft) { this.treeRendererLeft.expandedFolders = new Set(state.expandedFoldersLeft); } // Restaurer recherche AVANT le rendu if (this.treeRendererLeft && state.searchQueryLeft) { this.treeRendererLeft.searchQuery = state.searchQueryLeft; if (this.treeRendererLeft.searchInput) { this.treeRendererLeft.searchInput.value = state.searchQueryLeft; } if (state.searchQueryLeft && this.treeRendererLeft.searchClearBtn) { this.treeRendererLeft.searchClearBtn.classList.remove('hidden'); } } // Charger la structure (ne pas appeler onStructureLoaded car ça va déclencher saveState) const conflictPaths = this.state.conflicts.map(c => c.path); this.treeRendererLeft.render(state.leftStructure, conflictPaths); } // Restaurer right if (state.rightStructure) { this.state.rightStructure = state.rightStructure; this.state.rightStats = state.rightStats; // Afficher les stats dans l'UI this.displayUploadInfo('right', state.rightStats, state.rightFileName); // Restaurer expanded folders AVANT le rendu if (this.treeRendererRight && state.expandedFoldersRight) { this.treeRendererRight.expandedFolders = new Set(state.expandedFoldersRight); } // Restaurer recherche AVANT le rendu if (this.treeRendererRight && state.searchQueryRight) { this.treeRendererRight.searchQuery = state.searchQueryRight; if (this.treeRendererRight.searchInput) { this.treeRendererRight.searchInput.value = state.searchQueryRight; } if (state.searchQueryRight && this.treeRendererRight.searchClearBtn) { this.treeRendererRight.searchClearBtn.classList.remove('hidden'); } } // Charger la structure (ne pas appeler onStructureLoaded car ça va déclencher saveState) const conflictPaths = this.state.conflicts.map(c => c.path); this.treeRendererRight.render(state.rightStructure, conflictPaths); } // Mettre à jour les checkboxes et le compteur APRÈS les renders if (state.selection) { this.onSelectionChanged(); } // Mettre à jour le bouton merge this.updateMergeButton(); console.log('[FuZipApp] State restored'); } catch (error) { console.warn('[FuZipApp] Failed to restore state:', error); } } /** * Réinitialise l'application */ reset() { this.state = { leftStructure: null, rightStructure: null, leftStats: null, rightStats: null, conflicts: [], selection: new Map() }; this.uploadLeft?.reset(); this.uploadRight?.reset(); this.showConflictsBanner(0); this.updateMergeButton(); // Clear trees (Phase 6) this.treeRendererLeft?.clear(); this.treeRendererRight?.clear(); // Reset selection count if (this.selectionCount) { this.selectionCount.textContent = '0'; } // Effacer l'état sauvegardé localStorage.removeItem('fuzip_state'); console.log('[FuZipApp] State cleared'); } } // Initialisation au chargement du DOM document.addEventListener('DOMContentLoaded', () => { window.fuZipApp = new FuZipApp(); });