Initial commit
This commit is contained in:
commit
f2817bc1f1
17 changed files with 2785 additions and 0 deletions
296
ui/ui.js
Normal file
296
ui/ui.js
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
/**
|
||||
* 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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue