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
JavaScript
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();
});