UNPKG

empty-folder-manager

Version:

Interactive CLI tool for analyzing and removing empty folders from filesystem with .gitignore support and custom glob patterns

429 lines (360 loc) 15.8 kB
class EmptyFolderManagerUI { constructor() { this.config = { path: '', speed: 'normal', gitignoreEnabled: true, customPatterns: [] }; this.analysisData = null; this.isAnalyzing = false; this.currentPathHash = null; this.initializeElements(); this.bindEvents(); this.loadConfig(); this.loadExistingAnalysis(); } initializeElements() { // Configuration elements this.pathInput = document.getElementById('path-input'); this.browseBtn = document.getElementById('browse-btn'); this.speedSelect = document.getElementById('speed-select'); this.gitignoreCheckbox = document.getElementById('gitignore-enabled'); this.customPatternsTextarea = document.getElementById('custom-patterns'); // Action buttons this.analyzeBtn = document.getElementById('analyze-btn'); this.statsBtn = document.getElementById('stats-btn'); this.dryRunBtn = document.getElementById('dry-run-btn'); this.removeBtn = document.getElementById('remove-btn'); // Progress elements this.progressSection = document.querySelector('.progress-section'); this.progressFill = document.querySelector('.progress-fill'); this.progressText = document.querySelector('.progress-text'); // Results elements this.resultsSection = document.querySelector('.results-section'); this.totalFoldersEl = document.getElementById('total-folders'); this.ignoredFoldersEl = document.getElementById('ignored-folders'); this.processedFoldersEl = document.getElementById('processed-folders'); this.analysisTimeEl = document.getElementById('analysis-time'); this.folderList = document.getElementById('folder-list'); // Log elements this.logContainer = document.getElementById('log-container'); // Modal elements this.confirmationModal = document.getElementById('confirmation-modal'); this.confirmationMessage = document.getElementById('confirmation-message'); this.confirmYesBtn = document.getElementById('confirm-yes'); this.confirmNoBtn = document.getElementById('confirm-no'); } bindEvents() { // Configuration events this.pathInput.addEventListener('input', () => this.saveConfig()); this.speedSelect.addEventListener('change', () => this.saveConfig()); this.gitignoreCheckbox.addEventListener('change', () => this.saveConfig()); this.customPatternsTextarea.addEventListener('input', () => this.saveConfig()); // Action button events this.analyzeBtn.addEventListener('click', () => this.runAnalysis()); this.statsBtn.addEventListener('click', () => this.showStats()); this.dryRunBtn.addEventListener('click', () => this.runDryRun()); this.removeBtn.addEventListener('click', () => this.removeFolders()); // Browse button (note: file system access is limited in browsers) this.browseBtn.addEventListener('click', () => { this.addLog('info', '💡 Tip: Enter the full path manually (e.g., /home/user/Documents or C:\\Users\\User\\Documents)'); }); // Modal events this.confirmNoBtn.addEventListener('click', () => this.hideConfirmationModal()); } loadConfig() { const saved = localStorage.getItem('empty-folder-manager-config'); if (saved) { try { const config = JSON.parse(saved); this.config = { ...this.config, ...config }; this.pathInput.value = this.config.path || ''; this.speedSelect.value = this.config.speed || 'normal'; this.gitignoreCheckbox.checked = this.config.gitignoreEnabled !== false; this.customPatternsTextarea.value = (this.config.customPatterns || []).join('\n'); } catch (error) { this.addLog('error', 'Failed to load saved configuration'); } } } saveConfig() { this.config.path = this.pathInput.value.trim(); this.config.speed = this.speedSelect.value; this.config.gitignoreEnabled = this.gitignoreCheckbox.checked; this.config.customPatterns = this.customPatternsTextarea.value .split('\n') .map(p => p.trim()) .filter(p => p.length > 0); localStorage.setItem('empty-folder-manager-config', JSON.stringify(this.config)); // Load existing analysis when path changes this.loadExistingAnalysis(); } async loadExistingAnalysis() { if (!this.config.path) { this.analysisData = null; this.updateActionButtons(); this.showResults(false); return; } try { // Get path hash const hashResponse = await fetch('/api/path-hash', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: this.config.path }) }); if (!hashResponse.ok) return; const { pathHash } = await hashResponse.json(); this.currentPathHash = pathHash; // Try to load existing analysis const analysisResponse = await fetch(`/api/analysis/${pathHash}`); if (analysisResponse.ok) { this.analysisData = await analysisResponse.json(); this.addLog('info', `📊 Loaded existing analysis from ${new Date(this.analysisData.timestamp).toLocaleString()}`); this.displayResults(); this.updateActionButtons(); } else { this.analysisData = null; this.updateActionButtons(); this.showResults(false); } } catch (error) { // Silently fail - no existing analysis available this.analysisData = null; this.updateActionButtons(); this.showResults(false); } } addLog(type, message) { const logEntry = document.createElement('div'); logEntry.className = `log-entry ${type}`; logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; this.logContainer.appendChild(logEntry); this.logContainer.scrollTop = this.logContainer.scrollHeight; } showProgress(show = true) { this.progressSection.style.display = show ? 'block' : 'none'; if (!show) { this.progressFill.style.width = '0%'; this.progressText.textContent = 'Ready to start...'; } } updateProgress(percentage, text) { this.progressFill.style.width = `${percentage}%`; this.progressText.textContent = text; } showResults(show = true) { this.resultsSection.style.display = show ? 'block' : 'none'; } updateActionButtons() { const hasAnalysis = this.analysisData && this.analysisData.emptyFolders; this.statsBtn.disabled = !hasAnalysis; this.dryRunBtn.disabled = !hasAnalysis; this.removeBtn.disabled = !hasAnalysis; } async runAnalysis() { if (!this.config.path) { this.addLog('error', '❌ Please enter a directory path first'); return; } if (this.isAnalyzing) { this.addLog('warning', '⚠️ Analysis already in progress'); return; } this.isAnalyzing = true; this.analyzeBtn.disabled = true; this.showProgress(true); this.showResults(false); try { this.addLog('info', `🔍 Starting analysis of: ${this.config.path}`); this.updateProgress(10, 'Initializing analysis...'); const response = await fetch('/api/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.config) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // Handle streaming response const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); // Keep incomplete line in buffer for (const line of lines) { if (line.trim()) { try { const data = JSON.parse(line); this.handleAnalysisUpdate(data); } catch (error) { // Ignore malformed JSON lines } } } } // Process any remaining data if (buffer.trim()) { try { const data = JSON.parse(buffer); this.handleAnalysisUpdate(data); } catch (error) { // Ignore malformed JSON } } } catch (error) { this.addLog('error', `❌ Analysis failed: ${error.message}`); this.updateProgress(0, 'Analysis failed'); } finally { this.isAnalyzing = false; this.analyzeBtn.disabled = false; this.showProgress(false); } } handleAnalysisUpdate(data) { if (data.type === 'progress') { this.updateProgress(data.percentage, data.message); this.addLog('info', data.message); } else if (data.type === 'complete') { this.analysisData = data.result; this.updateProgress(100, 'Analysis complete!'); this.addLog('success', `✅ Analysis complete! Found ${data.result.emptyFolders.length} empty folders`); this.addLog('info', '💾 Analysis saved - results will persist after page refresh'); this.displayResults(); this.updateActionButtons(); } else if (data.type === 'error') { this.addLog('error', `❌ ${data.message}`); } } displayResults() { if (!this.analysisData) return; const { emptyFolders, totalProcessed, ignoredCount, duration } = this.analysisData; // Update stats this.totalFoldersEl.textContent = emptyFolders.length; this.ignoredFoldersEl.textContent = ignoredCount || 0; this.processedFoldersEl.textContent = totalProcessed || 0; this.analysisTimeEl.textContent = duration ? `${duration}s` : '0s'; // Update folder list this.folderList.innerHTML = ''; if (emptyFolders.length === 0) { this.folderList.innerHTML = '<div class="folder-item">🎉 No empty folders found! Your directory is clean.</div>'; } else { emptyFolders.slice(0, 100).forEach(folder => { // Limit to first 100 for performance const folderItem = document.createElement('div'); folderItem.className = 'folder-item'; folderItem.textContent = folder; this.folderList.appendChild(folderItem); }); if (emptyFolders.length > 100) { const moreItem = document.createElement('div'); moreItem.className = 'folder-item'; moreItem.style.fontStyle = 'italic'; moreItem.style.color = '#666'; moreItem.textContent = `... and ${emptyFolders.length - 100} more folders`; this.folderList.appendChild(moreItem); } } this.showResults(true); } showStats() { if (!this.analysisData) { this.addLog('error', '❌ No analysis data available'); return; } this.displayResults(); this.addLog('info', '📊 Displaying analysis statistics'); } async runDryRun() { if (!this.analysisData) { this.addLog('error', '❌ No analysis data available'); return; } this.addLog('info', '🔍 Running dry run (no folders will be deleted)'); try { const response = await fetch('/api/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...this.config, dryRun: true, folders: this.analysisData.emptyFolders }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); this.addLog('success', `🔍 Dry run complete: ${result.removedCount} folders would be removed`); } catch (error) { this.addLog('error', `❌ Dry run failed: ${error.message}`); } } removeFolders() { if (!this.analysisData) { this.addLog('error', '❌ No analysis data available'); return; } const folderCount = this.analysisData.emptyFolders.length; this.showConfirmationModal( `⚠️ This will permanently delete ${folderCount} empty folders. This action cannot be undone. Are you sure you want to proceed?`, () => this.performRemoval() ); } async performRemoval() { this.addLog('warning', '🗑️ Starting folder removal...'); try { const response = await fetch('/api/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...this.config, dryRun: false, folders: this.analysisData.emptyFolders }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); this.addLog('success', `✅ Removal complete: ${result.removedCount} folders deleted`); if (result.errorCount > 0) { this.addLog('warning', `⚠️ ${result.errorCount} folders could not be removed`); } // Clear analysis data since folders have been removed this.analysisData = null; this.updateActionButtons(); this.showResults(false); } catch (error) { this.addLog('error', `❌ Removal failed: ${error.message}`); } } showConfirmationModal(message, onConfirm) { this.confirmationMessage.textContent = message; this.confirmationModal.style.display = 'flex'; this.confirmYesBtn.onclick = () => { this.hideConfirmationModal(); onConfirm(); }; } hideConfirmationModal() { this.confirmationModal.style.display = 'none'; this.confirmYesBtn.onclick = null; } } // Initialize the application when the page loads document.addEventListener('DOMContentLoaded', () => { new EmptyFolderManagerUI(); });