Add dark mode UI and update components
This commit is contained in:
parent
f2817bc1f1
commit
c263092c10
9 changed files with 488 additions and 87 deletions
240
ui/ui.js
240
ui/ui.js
|
|
@ -8,6 +8,7 @@ class UIManager {
|
|||
this.currentFile = null;
|
||||
this.currentHistory = null;
|
||||
this.initializeElements();
|
||||
this.initializeTheme();
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
|
|
@ -37,10 +38,83 @@ class UIManager {
|
|||
|
||||
// Tree elements
|
||||
this.treeContainer = document.getElementById('treeContainer');
|
||||
this.refreshBtn = document.getElementById('refreshBtn');
|
||||
|
||||
// Status
|
||||
this.status = document.getElementById('status');
|
||||
|
||||
// Theme toggle button
|
||||
this.themeToggleBtn = document.getElementById('themeToggleBtn');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize theme based on system preference or saved preference
|
||||
*/
|
||||
initializeTheme() {
|
||||
// Check for saved theme preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
|
||||
if (savedTheme) {
|
||||
// Use saved preference
|
||||
this.setTheme(savedTheme);
|
||||
} else {
|
||||
// Check system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
this.setTheme(prefersDark ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
// Setup theme toggle button listener
|
||||
if (this.themeToggleBtn) {
|
||||
this.themeToggleBtn.addEventListener('click', () => this.toggleTheme());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the theme
|
||||
* @param {string} theme - 'light' or 'dark'
|
||||
*/
|
||||
setTheme(theme) {
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
this.updateThemeButton('☀️');
|
||||
this.updateHighlightTheme('dark');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
this.updateThemeButton('🌙');
|
||||
this.updateHighlightTheme('light');
|
||||
}
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update highlight.js theme based on current theme
|
||||
* @param {string} theme - 'light' or 'dark'
|
||||
*/
|
||||
updateHighlightTheme(theme) {
|
||||
const highlightLink = document.getElementById('highlightTheme');
|
||||
if (highlightLink) {
|
||||
const baseUrl = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/';
|
||||
const stylesheet = theme === 'dark' ? 'atom-one-dark.min.css' : 'atom-one-light.min.css';
|
||||
highlightLink.href = baseUrl + stylesheet;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between light and dark themes
|
||||
*/
|
||||
toggleTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
this.setTheme(newTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update theme toggle button icon
|
||||
* @param {string} icon - Icon emoji
|
||||
*/
|
||||
updateThemeButton(icon) {
|
||||
if (this.themeToggleBtn) {
|
||||
this.themeToggleBtn.textContent = icon;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -64,9 +138,51 @@ class UIManager {
|
|||
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>`;
|
||||
// Detect language from file extension
|
||||
const extension = path.split('.').pop().toLowerCase();
|
||||
const languageMap = {
|
||||
'js': 'javascript',
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'cpp': 'cpp',
|
||||
'c': 'c',
|
||||
'h': 'c',
|
||||
'hpp': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'rb': 'ruby',
|
||||
'go': 'go',
|
||||
'rs': 'rust',
|
||||
'php': 'php',
|
||||
'html': 'html',
|
||||
'xml': 'xml',
|
||||
'css': 'css',
|
||||
'json': 'json',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'sh': 'bash',
|
||||
'bash': 'bash',
|
||||
'sql': 'sql',
|
||||
'md': 'markdown',
|
||||
'txt': 'plaintext'
|
||||
};
|
||||
|
||||
const language = languageMap[extension] || 'plaintext';
|
||||
|
||||
// Create code element with language class
|
||||
const codeElement = document.createElement('code');
|
||||
codeElement.className = `language-${language}`;
|
||||
codeElement.textContent = content;
|
||||
|
||||
const preElement = document.createElement('pre');
|
||||
preElement.appendChild(codeElement);
|
||||
|
||||
this.fileContent.innerHTML = '';
|
||||
this.fileContent.appendChild(preElement);
|
||||
|
||||
// Apply syntax highlighting
|
||||
if (window.hljs) {
|
||||
hljs.highlightElement(codeElement);
|
||||
}
|
||||
|
||||
this.showView(this.fileView);
|
||||
}
|
||||
|
|
@ -119,24 +235,22 @@ class UIManager {
|
|||
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>`;
|
||||
// Create code element with diff language class
|
||||
const codeElement = document.createElement('code');
|
||||
codeElement.className = 'language-diff';
|
||||
codeElement.textContent = diffText;
|
||||
|
||||
const preElement = document.createElement('pre');
|
||||
preElement.appendChild(codeElement);
|
||||
|
||||
this.diffContent.innerHTML = '';
|
||||
this.diffContent.appendChild(preElement);
|
||||
|
||||
// Apply syntax highlighting
|
||||
if (window.hljs) {
|
||||
hljs.highlightElement(codeElement);
|
||||
}
|
||||
|
||||
this.showView(this.diffView);
|
||||
}
|
||||
|
||||
|
|
@ -237,13 +351,14 @@ class UIManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Recursively render tree nodes
|
||||
* Recursively render tree nodes with collapsible folders
|
||||
* @param {object} node - Tree node
|
||||
* @param {string} path - Current path
|
||||
* @param {HTMLElement} container - Container element
|
||||
* @param {Function} onFileClick - Callback when file is clicked
|
||||
* @param {number} depth - Current depth level for indentation
|
||||
*/
|
||||
renderTreeNode(node, path, container, onFileClick) {
|
||||
renderTreeNode(node, path, container, onFileClick, depth = 0) {
|
||||
const keys = Object.keys(node).sort();
|
||||
|
||||
keys.forEach(key => {
|
||||
|
|
@ -252,33 +367,78 @@ class UIManager {
|
|||
|
||||
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);
|
||||
item.style.paddingLeft = `${0.5 + depth * 1.25}rem`;
|
||||
|
||||
if (isFile) {
|
||||
// File item
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'tree-item-icon';
|
||||
icon.textContent = '📄';
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'tree-item-name';
|
||||
name.textContent = key;
|
||||
|
||||
item.appendChild(icon);
|
||||
item.appendChild(name);
|
||||
|
||||
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);
|
||||
});
|
||||
} else {
|
||||
// Folder item with toggle
|
||||
const toggleBtn = document.createElement('span');
|
||||
toggleBtn.className = 'tree-item-toggle';
|
||||
toggleBtn.textContent = '▼';
|
||||
toggleBtn.style.cursor = 'pointer';
|
||||
toggleBtn.style.marginRight = '0.25rem';
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'tree-item-icon';
|
||||
icon.textContent = '📁';
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'tree-item-name';
|
||||
name.textContent = key;
|
||||
|
||||
item.appendChild(toggleBtn);
|
||||
item.appendChild(icon);
|
||||
item.appendChild(name);
|
||||
|
||||
// Create container for nested items
|
||||
const nestedContainer = document.createElement('div');
|
||||
nestedContainer.className = 'tree-nested';
|
||||
nestedContainer.style.display = 'block';
|
||||
|
||||
// Add toggle functionality
|
||||
const toggleFolder = () => {
|
||||
const isExpanded = nestedContainer.style.display !== 'none';
|
||||
nestedContainer.style.display = isExpanded ? 'none' : 'block';
|
||||
toggleBtn.textContent = isExpanded ? '▶' : '▼';
|
||||
};
|
||||
|
||||
toggleBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleFolder();
|
||||
});
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleFolder();
|
||||
});
|
||||
|
||||
container.appendChild(item);
|
||||
container.appendChild(nestedContainer);
|
||||
|
||||
// Recursively render subdirectories
|
||||
this.renderTreeNode(node[key], fullPath, nestedContainer, onFileClick, depth + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
container.appendChild(item);
|
||||
|
||||
// Recursively render subdirectories
|
||||
if (!isFile) {
|
||||
this.renderTreeNode(node[key], fullPath, container, onFileClick);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue