Add dark mode UI and update components

This commit is contained in:
Juan José Gutiérrez de Quevedo Pérez 2025-11-21 13:09:47 +01:00
parent f2817bc1f1
commit c263092c10
9 changed files with 488 additions and 87 deletions

240
ui/ui.js
View file

@ -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);
}
});
}