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
39
Dockerfile
39
Dockerfile
|
|
@ -1,52 +1,29 @@
|
|||
FROM python:3.13-slim AS builder
|
||||
FROM python:3-bookworm
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies for CVS
|
||||
RUN apt-get update && \
|
||||
apt-get install -y cvs && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
# Install runtime dependencies (minimal)
|
||||
RUN apt update && apt install cvs rsh-client
|
||||
|
||||
# Copy only requirements first to leverage docker cache
|
||||
COPY requirements.txt .
|
||||
COPY . .
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy the rest of the application
|
||||
COPY . .
|
||||
|
||||
# Install the package in editable mode
|
||||
RUN pip install --no-cache-dir -e .
|
||||
|
||||
# Final stage
|
||||
FROM python:3.13-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install CVS
|
||||
RUN apt-get update && \
|
||||
apt-get install -y cvs && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy only necessary files from builder
|
||||
COPY --from=builder /app /app
|
||||
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
|
||||
|
||||
# Set environment variables for configuration (with defaults)
|
||||
ENV CVS_URL="" \
|
||||
REPO_CHECKOUTS=/tmp/cvs_checkouts \
|
||||
CVS_MODULE="" \
|
||||
BASEPATH="" \
|
||||
FLASK_HOST=0.0.0.0 \
|
||||
FLASK_PORT=5000 \
|
||||
FLASK_DEBUG=false
|
||||
FLASK_DEBUG=false \
|
||||
CVS_RSH=rsh
|
||||
|
||||
# Expose the application port
|
||||
EXPOSE 5000
|
||||
|
||||
# Set the entrypoint to run the application with environment variables as command-line arguments
|
||||
ENTRYPOINT ["sh", "-c", "python -m cvs_proxy.app --cvs-url \"$CVS_URL\" --repo-checkouts \"$REPO_CHECKOUTS\" ${CVS_MODULE:+--cvs-module \"$CVS_MODULE\"} --host \"$FLASK_HOST\" --port \"$FLASK_PORT\" ${FLASK_DEBUG:+--debug}"]
|
||||
ENTRYPOINT ["sh", "-c", "python -m cvs_proxy.app --cvs-url \"$CVS_URL\" --repo-checkouts \"$REPO_CHECKOUTS\" ${CVS_MODULE:+--cvs-module \"$CVS_MODULE\"} ${BASEPATH:+--basepath \"$BASEPATH\"} --host \"$FLASK_HOST\" --port \"$FLASK_PORT\" ${FLASK_DEBUG:+--debug}"]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from flask import Flask, request, jsonify, send_from_directory
|
||||
from flask import Flask, request, jsonify, send_from_directory, Blueprint
|
||||
from .cvs_client import CVSClient
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -17,6 +17,9 @@ app = Flask(__name__, static_folder=ui_dir, static_url_path='')
|
|||
cvs_client = None
|
||||
app_config = {}
|
||||
|
||||
# Create API Blueprint (will be registered with basepath as url_prefix)
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
||||
# Initialize CVS Client using provided parameters
|
||||
def create_cvs_client(cvs_url, repos_checkout=None, cvs_module=None):
|
||||
"""
|
||||
|
|
@ -37,7 +40,17 @@ def create_cvs_client(cvs_url, repos_checkout=None, cvs_module=None):
|
|||
|
||||
return CVSClient(cvs_url, repos_checkout=repos_checkout, cvs_module=cvs_module)
|
||||
|
||||
@app.route('/v1/tree', methods=['GET'])
|
||||
@api_bp.route('/ui/config.js', methods=['GET'])
|
||||
def serve_config():
|
||||
"""
|
||||
Serve dynamic configuration as JavaScript
|
||||
This allows the frontend to access the basepath configuration
|
||||
"""
|
||||
basepath = app_config.get('basepath', '')
|
||||
config_js = f"window.APP_CONFIG = {{ basepath: '{basepath}' }};"
|
||||
return config_js, 200, {'Content-Type': 'application/javascript'}
|
||||
|
||||
@api_bp.route('/api/v1/tree', methods=['GET'])
|
||||
def get_repository_tree():
|
||||
"""
|
||||
Get repository tree structure
|
||||
|
|
@ -53,7 +66,7 @@ def get_repository_tree():
|
|||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/v1/diff', methods=['GET'])
|
||||
@api_bp.route('/api/v1/diff', methods=['GET'])
|
||||
def get_file_diff():
|
||||
"""
|
||||
Get diff between two revisions of a file
|
||||
|
|
@ -75,7 +88,7 @@ def get_file_diff():
|
|||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/v1/history', methods=['GET'])
|
||||
@api_bp.route('/api/v1/history', methods=['GET'])
|
||||
def get_file_history():
|
||||
"""
|
||||
Get revision history for a file
|
||||
|
|
@ -95,7 +108,7 @@ def get_file_history():
|
|||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/v1/file', methods=['GET'])
|
||||
@api_bp.route('/api/v1/file', methods=['GET'])
|
||||
def get_file_content():
|
||||
"""
|
||||
Get raw file content at a specific revision
|
||||
|
|
@ -117,7 +130,7 @@ def get_file_content():
|
|||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/v1/health', methods=['GET'])
|
||||
@api_bp.route('/api/v1/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""
|
||||
Simple health check endpoint
|
||||
|
|
@ -127,14 +140,14 @@ def health_check():
|
|||
"cvs_client": "initialized" if cvs_client else "not initialized"
|
||||
})
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
@api_bp.route('/ui/', methods=['GET'])
|
||||
def index():
|
||||
"""
|
||||
Serve the UI index.html
|
||||
"""
|
||||
return send_from_directory(ui_dir, 'index.html')
|
||||
|
||||
@app.route('/<path:filename>', methods=['GET'])
|
||||
@api_bp.route('/ui/<path:filename>', methods=['GET'])
|
||||
def serve_static(filename):
|
||||
"""
|
||||
Serve static files (CSS, JS, etc.)
|
||||
|
|
@ -180,6 +193,11 @@ def main():
|
|||
default=5000,
|
||||
help='Flask port to bind to (default: 5000)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--basepath',
|
||||
default='',
|
||||
help='Base path for API requests (e.g., /api/cvs) (default: empty string for root)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
|
|
@ -195,9 +213,14 @@ def main():
|
|||
'cvs_module': args.cvs_module,
|
||||
'host': args.host,
|
||||
'port': args.port,
|
||||
'basepath': args.basepath,
|
||||
'debug': args.debug
|
||||
}
|
||||
|
||||
# Register the API Blueprint with basepath as url_prefix
|
||||
basepath = args.basepath if args.basepath else ''
|
||||
app.register_blueprint(api_bp, url_prefix=basepath)
|
||||
|
||||
# Attempt to create CVS client at startup
|
||||
try:
|
||||
cvs_client = create_cvs_client(
|
||||
|
|
@ -209,7 +232,7 @@ def main():
|
|||
print(f"Error initializing CVS Client: {e}", file=sys.stderr)
|
||||
cvs_client = None
|
||||
|
||||
print(f"Starting CVS Proxy on {args.host}:{args.port}")
|
||||
print(f"Starting CVS Proxy on {args.host}:{args.port} with basepath: '{basepath}'")
|
||||
app.run(host=args.host, port=args.port, debug=args.debug)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
107
ui/DARK_MODE_README.md
Normal file
107
ui/DARK_MODE_README.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Dark Mode Implementation
|
||||
|
||||
## Overview
|
||||
The CVS Repository Browser now includes a fully functional dark mode with system theme detection and user preference persistence.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. **System Theme Detection**
|
||||
- On first load, the application automatically detects the user's system theme preference using `prefers-color-scheme` media query
|
||||
- If the system prefers dark mode, the app starts in dark mode
|
||||
- If the system prefers light mode, the app starts in light mode
|
||||
- If no system preference is detected, defaults to dark mode
|
||||
|
||||
### 2. **User Preference Persistence**
|
||||
- User's theme choice is saved to browser's localStorage
|
||||
- On subsequent visits, the saved preference is used instead of system preference
|
||||
- Users can override system preference by toggling the theme
|
||||
|
||||
### 3. **Theme Toggle Button**
|
||||
- Located in the header next to the status indicator
|
||||
- Shows 🌙 (moon) icon in light mode
|
||||
- Shows ☀️ (sun) icon in dark mode
|
||||
- Click to toggle between light and dark themes
|
||||
|
||||
### 4. **Comprehensive Dark Mode Styling**
|
||||
- All UI elements are properly styled for both light and dark modes
|
||||
- Uses CSS custom properties (variables) for easy theme switching
|
||||
- Maintains excellent contrast ratios for accessibility
|
||||
- Smooth transitions between themes
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### CSS Variables
|
||||
The application uses CSS custom properties defined in `:root` and `[data-theme="dark"]` selectors:
|
||||
|
||||
**Light Mode (default):**
|
||||
- Background: `#f9fafb` (light gray)
|
||||
- Surface: `#ffffff` (white)
|
||||
- Text Primary: `#111827` (dark gray)
|
||||
- Text Secondary: `#6b7280` (medium gray)
|
||||
- Border: `#e5e7eb` (light border)
|
||||
|
||||
**Dark Mode:**
|
||||
- Background: `#1a1a1a` (very dark)
|
||||
- Surface: `#2d2d2d` (dark gray)
|
||||
- Text Primary: `#f3f4f6` (light gray)
|
||||
- Text Secondary: `#d1d5db` (medium light gray)
|
||||
- Border: `#404040` (dark border)
|
||||
|
||||
### JavaScript Implementation
|
||||
The `UIManager` class in `ui.js` handles theme management:
|
||||
|
||||
- `initializeTheme()`: Initializes theme on page load
|
||||
- `setTheme(theme)`: Sets the theme and updates localStorage
|
||||
- `toggleTheme()`: Toggles between light and dark modes
|
||||
- `updateThemeButton(icon)`: Updates the toggle button icon
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **styles.css**
|
||||
- Added `[data-theme="dark"]` selector with dark mode CSS variables
|
||||
- All color references use CSS variables for dynamic theming
|
||||
|
||||
2. **index.html**
|
||||
- Added theme toggle button in header: `<button id="themeToggleBtn" class="btn-icon">🌙</button>`
|
||||
|
||||
3. **ui.js**
|
||||
- Added `initializeTheme()` method to detect and apply system theme
|
||||
- Added `setTheme()` method to apply theme and save preference
|
||||
- Added `toggleTheme()` method to switch between themes
|
||||
- Added `updateThemeButton()` method to update button icon
|
||||
- Added theme toggle button event listener
|
||||
|
||||
## Usage
|
||||
|
||||
### For Users
|
||||
1. The app automatically uses your system theme preference on first visit
|
||||
2. Click the moon/sun icon in the header to toggle between light and dark modes
|
||||
3. Your preference is saved and will be remembered on future visits
|
||||
|
||||
### For Developers
|
||||
To modify the dark mode colors, edit the CSS variables in `styles.css`:
|
||||
|
||||
```css
|
||||
[data-theme="dark"] {
|
||||
--primary-color: #3b82f6;
|
||||
--primary-hover: #2563eb;
|
||||
--secondary-color: #9ca3af;
|
||||
/* ... other variables ... */
|
||||
}
|
||||
```
|
||||
|
||||
## Browser Compatibility
|
||||
- Works in all modern browsers that support:
|
||||
- CSS Custom Properties (CSS Variables)
|
||||
- `prefers-color-scheme` media query
|
||||
- localStorage API
|
||||
- ES6 JavaScript
|
||||
|
||||
## Testing
|
||||
A test file `test-dark-mode.html` is included for testing the dark mode functionality independently.
|
||||
|
||||
To test:
|
||||
1. Open `test-dark-mode.html` in a browser
|
||||
2. Click the moon/sun button to toggle themes
|
||||
3. Refresh the page to verify persistence
|
||||
4. Check browser DevTools to see localStorage values
|
||||
|
|
@ -15,7 +15,7 @@ class CVSProxyAPI {
|
|||
* @returns {Promise} Response data
|
||||
*/
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseURL}/v1${endpoint}`;
|
||||
const url = `${this.baseURL}/api/v1${endpoint}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
|
|
@ -108,5 +108,5 @@ class CVSProxyAPI {
|
|||
}
|
||||
}
|
||||
|
||||
// Create global API instance
|
||||
const api = new CVSProxyAPI();
|
||||
// Create global API instance with basepath from config
|
||||
const api = new CVSProxyAPI(window.APP_CONFIG?.basepath || '');
|
||||
|
|
@ -162,9 +162,6 @@ class CVSRepositoryBrowser {
|
|||
* Setup event listeners
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Refresh button
|
||||
ui.refreshBtn.addEventListener('click', () => this.loadTree());
|
||||
|
||||
// File view buttons
|
||||
ui.historyBtn.addEventListener('click', () => this.showHistory());
|
||||
ui.diffBtn.addEventListener('click', () => this.showDiffView());
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CVS Proxy - Repository Browser</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link id="highlightTheme" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>CVS Repository Browser</h1>
|
||||
<div class="header-info">
|
||||
<button id="themeToggleBtn" class="btn-icon" title="Toggle dark mode">🌙</button>
|
||||
<span id="status" class="status">Connecting...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -21,7 +25,6 @@
|
|||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>Repository</h2>
|
||||
<button id="refreshBtn" class="btn-icon" title="Refresh">🔄</button>
|
||||
</div>
|
||||
<div id="treeContainer" class="tree-container">
|
||||
<div class="loading">Loading repository...</div>
|
||||
|
|
@ -105,6 +108,7 @@
|
|||
</main>
|
||||
</div>
|
||||
|
||||
<script src="config.js"></script>
|
||||
<script src="api.js"></script>
|
||||
<script src="ui.js"></script>
|
||||
<script src="app.js"></script>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,23 @@
|
|||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Dark mode theme */
|
||||
[data-theme="dark"] {
|
||||
--primary-color: #3b82f6;
|
||||
--primary-hover: #2563eb;
|
||||
--secondary-color: #9ca3af;
|
||||
--success-color: #10b981;
|
||||
--danger-color: #ef4444;
|
||||
--warning-color: #f59e0b;
|
||||
--bg-color: #1a1a1a;
|
||||
--surface-color: #2d2d2d;
|
||||
--border-color: #404040;
|
||||
--text-primary: #f3f4f6;
|
||||
--text-secondary: #d1d5db;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
|
|
@ -163,6 +180,16 @@ main {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tree-nested {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tree-item-toggle {
|
||||
display: inline-block;
|
||||
min-width: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
|
|
|
|||
106
ui/test-dark-mode.html
Normal file
106
ui/test-dark-mode.html
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dark Mode Test</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Dark Mode Test</h1>
|
||||
<div class="header-info">
|
||||
<button id="themeToggleBtn" class="btn-icon" title="Toggle dark mode">🌙</button>
|
||||
<span id="status" class="status connected">Theme: Light</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>Test Panel</h2>
|
||||
</div>
|
||||
<div class="tree-container">
|
||||
<div class="tree-item">
|
||||
<span class="tree-item-icon">📁</span>
|
||||
<span class="tree-item-name">Sample Folder</span>
|
||||
</div>
|
||||
<div class="tree-item">
|
||||
<span class="tree-item-icon">📄</span>
|
||||
<span class="tree-item-name">Sample File</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="main-content">
|
||||
<div class="view">
|
||||
<div class="file-header">
|
||||
<div class="file-info">
|
||||
<h2>Dark Mode Test</h2>
|
||||
<p class="file-path">This page tests the dark mode implementation</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-content">
|
||||
<pre><code>// Test code block
|
||||
function testDarkMode() {
|
||||
console.log('Dark mode is working!');
|
||||
return true;
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
class ThemeManager {
|
||||
constructor() {
|
||||
this.themeToggleBtn = document.getElementById('themeToggleBtn');
|
||||
this.status = document.getElementById('status');
|
||||
this.initializeTheme();
|
||||
}
|
||||
|
||||
initializeTheme() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
|
||||
if (savedTheme) {
|
||||
this.setTheme(savedTheme);
|
||||
} else {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
this.setTheme(prefersDark ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
if (this.themeToggleBtn) {
|
||||
this.themeToggleBtn.addEventListener('click', () => this.toggleTheme());
|
||||
}
|
||||
}
|
||||
|
||||
setTheme(theme) {
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
this.themeToggleBtn.textContent = '☀️';
|
||||
this.status.textContent = 'Theme: Dark';
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
this.themeToggleBtn.textContent = '🌙';
|
||||
this.status.textContent = 'Theme: Light';
|
||||
}
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
|
||||
toggleTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
this.setTheme(newTheme);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new ThemeManager();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
238
ui/ui.js
238
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('');
|
||||
// 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.diffContent.innerHTML = `<pre><code>${colorizedLines}</code></pre>`;
|
||||
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