296 lines
No EOL
9.9 KiB
JavaScript
296 lines
No EOL
9.9 KiB
JavaScript
/**
|
|
* 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 = `<pre><code>${escapedContent}</code></pre>`;
|
|
|
|
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 = '<div class="loading">No history available</div>';
|
|
this.showView(this.historyView);
|
|
return;
|
|
}
|
|
|
|
const historyHTML = history.map(revision => `
|
|
<div class="history-item" data-revision="${revision.revision}">
|
|
<div class="history-item-header">
|
|
<span class="history-revision">${revision.revision}</span>
|
|
<span class="history-date">${revision.date || 'N/A'}</span>
|
|
</div>
|
|
<div class="history-author">Author: <strong>${revision.author || 'Unknown'}</strong></div>
|
|
<span class="history-state">${revision.state || 'Exp'}</span>
|
|
<div class="history-lines">${revision.lines_changed || 'N/A'}</div>
|
|
</div>
|
|
`).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 = '<div class="loading">No differences found</div>';
|
|
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 `<div class="${className}">${escapedLine}</div>`;
|
|
}).join('');
|
|
|
|
this.diffContent.innerHTML = `<pre><code>${colorizedLines}</code></pre>`;
|
|
this.showView(this.diffView);
|
|
}
|
|
|
|
/**
|
|
* Populate revision selectors
|
|
* @param {Array} history - Array of revision objects
|
|
*/
|
|
populateRevisionSelectors(history) {
|
|
if (!history || history.length === 0) {
|
|
this.rev1Select.innerHTML = '<option>No revisions available</option>';
|
|
this.rev2Select.innerHTML = '<option>No revisions available</option>';
|
|
return;
|
|
}
|
|
|
|
const options = history.map(rev =>
|
|
`<option value="${rev.revision}">${rev.revision} - ${rev.date || 'N/A'}</option>`
|
|
).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 = `<div class="loading">${message}</div>`;
|
|
}
|
|
|
|
/**
|
|
* Show error message
|
|
* @param {string} message - Error message
|
|
*/
|
|
showError(message) {
|
|
this.fileContent.innerHTML = `<div class="loading" style="color: #991b1b;">${this.escapeHtml(message)}</div>`;
|
|
}
|
|
|
|
/**
|
|
* 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 = '<div class="loading">No files found</div>';
|
|
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(); |