/** * UI Management Module * Handles DOM manipulation and view switching */ class UIManager { constructor() { this.currentFile = null; this.currentHistory = null; this.initializeElements(); } initializeElements() { // Views this.welcomeView = document.getElementById('welcomeView'); this.fileView = document.getElementById('fileView'); this.historyView = document.getElementById('historyView'); this.diffView = document.getElementById('diffView'); // File view elements this.fileName = document.getElementById('fileName'); this.filePath = document.getElementById('filePath'); this.fileContent = document.getElementById('fileContent'); this.historyBtn = document.getElementById('historyBtn'); this.diffBtn = document.getElementById('diffBtn'); // History view elements this.historyContent = document.getElementById('historyContent'); this.backFromHistoryBtn = document.getElementById('backFromHistoryBtn'); // Diff view elements this.rev1Select = document.getElementById('rev1Select'); this.rev2Select = document.getElementById('rev2Select'); this.generateDiffBtn = document.getElementById('generateDiffBtn'); this.diffContent = document.getElementById('diffContent'); this.backFromDiffBtn = document.getElementById('backFromDiffBtn'); // Tree elements this.treeContainer = document.getElementById('treeContainer'); this.refreshBtn = document.getElementById('refreshBtn'); // Status this.status = document.getElementById('status'); } /** * Show a specific view and hide others * @param {HTMLElement} view - View to show */ showView(view) { [this.welcomeView, this.fileView, this.historyView, this.diffView].forEach(v => { if (v) v.classList.add('hidden'); }); if (view) view.classList.remove('hidden'); } /** * Display file content * @param {string} path - File path * @param {string} content - File content */ displayFile(path, content) { this.currentFile = path; this.fileName.textContent = path.split('/').pop(); this.filePath.textContent = path; // Escape HTML and display content const escapedContent = this.escapeHtml(content); this.fileContent.innerHTML = `
${escapedContent}
`; this.showView(this.fileView); } /** * Display file history * @param {Array} history - Array of revision objects */ displayHistory(history) { this.currentHistory = history; if (!history || history.length === 0) { this.historyContent.innerHTML = '
No history available
'; this.showView(this.historyView); return; } const historyHTML = history.map(revision => `
${revision.revision} ${revision.date || 'N/A'}
Author: ${revision.author || 'Unknown'}
${revision.state || 'Exp'}
${revision.lines_changed || 'N/A'}
`).join(''); this.historyContent.innerHTML = historyHTML; this.showView(this.historyView); // Add click handlers to history items this.historyContent.querySelectorAll('.history-item').forEach(item => { item.addEventListener('click', () => { const revision = item.dataset.revision; window.app.loadFileAtRevision(this.currentFile, revision); }); }); } /** * Display diff * @param {string} diffText - Diff content */ displayDiff(diffText) { if (!diffText) { this.diffContent.innerHTML = '
No differences found
'; this.showView(this.diffView); return; } // Parse and colorize diff const lines = diffText.split('\n'); const colorizedLines = lines.map(line => { let className = ''; if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@')) { className = 'diff-line header'; } else if (line.startsWith('+')) { className = 'diff-line added'; } else if (line.startsWith('-')) { className = 'diff-line removed'; } else { className = 'diff-line context'; } const escapedLine = this.escapeHtml(line); return `
${escapedLine}
`; }).join(''); this.diffContent.innerHTML = `
${colorizedLines}
`; this.showView(this.diffView); } /** * Populate revision selectors * @param {Array} history - Array of revision objects */ populateRevisionSelectors(history) { if (!history || history.length === 0) { this.rev1Select.innerHTML = ''; this.rev2Select.innerHTML = ''; return; } const options = history.map(rev => `` ).join(''); this.rev1Select.innerHTML = options; this.rev2Select.innerHTML = options; // Set default selections if (history.length > 1) { this.rev1Select.selectedIndex = history.length - 1; this.rev2Select.selectedIndex = 0; } } /** * Update status indicator * @param {string} message - Status message * @param {string} type - Status type: 'connecting', 'connected', 'error' */ updateStatus(message, type = 'connecting') { this.status.textContent = message; this.status.className = `status ${type}`; } /** * Show loading state * @param {string} message - Loading message */ showLoading(message = 'Loading...') { this.fileContent.innerHTML = `
${message}
`; } /** * Show error message * @param {string} message - Error message */ showError(message) { this.fileContent.innerHTML = `
${this.escapeHtml(message)}
`; } /** * Escape HTML special characters * @param {string} text - Text to escape * @returns {string} Escaped text */ escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } /** * Build tree view from file list * @param {Array} files - Array of file paths * @param {Function} onFileClick - Callback when file is clicked */ buildTree(files, onFileClick) { if (!files || files.length === 0) { this.treeContainer.innerHTML = '
No files found
'; return; } // Build hierarchical tree structure const tree = {}; files.forEach(file => { const parts = file.split('/'); let current = tree; parts.forEach((part, index) => { if (!current[part]) { current[part] = {}; } current = current[part]; }); }); // Render tree this.treeContainer.innerHTML = ''; this.renderTreeNode(tree, '', this.treeContainer, onFileClick); } /** * Recursively render tree nodes * @param {object} node - Tree node * @param {string} path - Current path * @param {HTMLElement} container - Container element * @param {Function} onFileClick - Callback when file is clicked */ renderTreeNode(node, path, container, onFileClick) { const keys = Object.keys(node).sort(); keys.forEach(key => { const fullPath = path ? `${path}/${key}` : key; const isFile = Object.keys(node[key]).length === 0; const item = document.createElement('div'); item.className = 'tree-item'; const icon = document.createElement('span'); icon.className = 'tree-item-icon'; icon.textContent = isFile ? '📄' : '📁'; const name = document.createElement('span'); name.className = 'tree-item-name'; name.textContent = key; item.appendChild(icon); item.appendChild(name); if (isFile) { item.addEventListener('click', () => { // Remove active class from all items container.querySelectorAll('.tree-item').forEach(i => i.classList.remove('active')); item.classList.add('active'); onFileClick(fullPath); }); } container.appendChild(item); // Recursively render subdirectories if (!isFile) { this.renderTreeNode(node[key], fullPath, container, onFileClick); } }); } /** * Clear active selection in tree */ clearTreeSelection() { this.treeContainer.querySelectorAll('.tree-item').forEach(item => { item.classList.remove('active'); }); } } // Create global UI manager instance const ui = new UIManager();