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
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies for CVS
|
# Install runtime dependencies (minimal)
|
||||||
RUN apt-get update && \
|
RUN apt update && apt install cvs rsh-client
|
||||||
apt-get install -y cvs && \
|
|
||||||
apt-get clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy only requirements first to leverage docker cache
|
COPY . .
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
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 .
|
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)
|
# Set environment variables for configuration (with defaults)
|
||||||
ENV CVS_URL="" \
|
ENV CVS_URL="" \
|
||||||
REPO_CHECKOUTS=/tmp/cvs_checkouts \
|
REPO_CHECKOUTS=/tmp/cvs_checkouts \
|
||||||
CVS_MODULE="" \
|
CVS_MODULE="" \
|
||||||
|
BASEPATH="" \
|
||||||
FLASK_HOST=0.0.0.0 \
|
FLASK_HOST=0.0.0.0 \
|
||||||
FLASK_PORT=5000 \
|
FLASK_PORT=5000 \
|
||||||
FLASK_DEBUG=false
|
FLASK_DEBUG=false \
|
||||||
|
CVS_RSH=rsh
|
||||||
|
|
||||||
# Expose the application port
|
# Expose the application port
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
# Set the entrypoint to run the application with environment variables as command-line arguments
|
# 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
|
from .cvs_client import CVSClient
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -17,6 +17,9 @@ app = Flask(__name__, static_folder=ui_dir, static_url_path='')
|
||||||
cvs_client = None
|
cvs_client = None
|
||||||
app_config = {}
|
app_config = {}
|
||||||
|
|
||||||
|
# Create API Blueprint (will be registered with basepath as url_prefix)
|
||||||
|
api_bp = Blueprint('api', __name__)
|
||||||
|
|
||||||
# Initialize CVS Client using provided parameters
|
# Initialize CVS Client using provided parameters
|
||||||
def create_cvs_client(cvs_url, repos_checkout=None, cvs_module=None):
|
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)
|
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():
|
def get_repository_tree():
|
||||||
"""
|
"""
|
||||||
Get repository tree structure
|
Get repository tree structure
|
||||||
|
|
@ -53,7 +66,7 @@ def get_repository_tree():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/v1/diff', methods=['GET'])
|
@api_bp.route('/api/v1/diff', methods=['GET'])
|
||||||
def get_file_diff():
|
def get_file_diff():
|
||||||
"""
|
"""
|
||||||
Get diff between two revisions of a file
|
Get diff between two revisions of a file
|
||||||
|
|
@ -75,7 +88,7 @@ def get_file_diff():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/v1/history', methods=['GET'])
|
@api_bp.route('/api/v1/history', methods=['GET'])
|
||||||
def get_file_history():
|
def get_file_history():
|
||||||
"""
|
"""
|
||||||
Get revision history for a file
|
Get revision history for a file
|
||||||
|
|
@ -95,7 +108,7 @@ def get_file_history():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/v1/file', methods=['GET'])
|
@api_bp.route('/api/v1/file', methods=['GET'])
|
||||||
def get_file_content():
|
def get_file_content():
|
||||||
"""
|
"""
|
||||||
Get raw file content at a specific revision
|
Get raw file content at a specific revision
|
||||||
|
|
@ -117,7 +130,7 @@ def get_file_content():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/v1/health', methods=['GET'])
|
@api_bp.route('/api/v1/health', methods=['GET'])
|
||||||
def health_check():
|
def health_check():
|
||||||
"""
|
"""
|
||||||
Simple health check endpoint
|
Simple health check endpoint
|
||||||
|
|
@ -127,14 +140,14 @@ def health_check():
|
||||||
"cvs_client": "initialized" if cvs_client else "not initialized"
|
"cvs_client": "initialized" if cvs_client else "not initialized"
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/', methods=['GET'])
|
@api_bp.route('/ui/', methods=['GET'])
|
||||||
def index():
|
def index():
|
||||||
"""
|
"""
|
||||||
Serve the UI index.html
|
Serve the UI index.html
|
||||||
"""
|
"""
|
||||||
return send_from_directory(ui_dir, '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):
|
def serve_static(filename):
|
||||||
"""
|
"""
|
||||||
Serve static files (CSS, JS, etc.)
|
Serve static files (CSS, JS, etc.)
|
||||||
|
|
@ -180,6 +193,11 @@ def main():
|
||||||
default=5000,
|
default=5000,
|
||||||
help='Flask port to bind to (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(
|
parser.add_argument(
|
||||||
'--debug',
|
'--debug',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
|
|
@ -195,9 +213,14 @@ def main():
|
||||||
'cvs_module': args.cvs_module,
|
'cvs_module': args.cvs_module,
|
||||||
'host': args.host,
|
'host': args.host,
|
||||||
'port': args.port,
|
'port': args.port,
|
||||||
|
'basepath': args.basepath,
|
||||||
'debug': args.debug
|
'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
|
# Attempt to create CVS client at startup
|
||||||
try:
|
try:
|
||||||
cvs_client = create_cvs_client(
|
cvs_client = create_cvs_client(
|
||||||
|
|
@ -209,7 +232,7 @@ def main():
|
||||||
print(f"Error initializing CVS Client: {e}", file=sys.stderr)
|
print(f"Error initializing CVS Client: {e}", file=sys.stderr)
|
||||||
cvs_client = None
|
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)
|
app.run(host=args.host, port=args.port, debug=args.debug)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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
|
* @returns {Promise} Response data
|
||||||
*/
|
*/
|
||||||
async request(endpoint, options = {}) {
|
async request(endpoint, options = {}) {
|
||||||
const url = `${this.baseURL}/v1${endpoint}`;
|
const url = `${this.baseURL}/api/v1${endpoint}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
|
|
@ -108,5 +108,5 @@ class CVSProxyAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create global API instance
|
// Create global API instance with basepath from config
|
||||||
const api = new CVSProxyAPI();
|
const api = new CVSProxyAPI(window.APP_CONFIG?.basepath || '');
|
||||||
|
|
@ -162,9 +162,6 @@ class CVSRepositoryBrowser {
|
||||||
* Setup event listeners
|
* Setup event listeners
|
||||||
*/
|
*/
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Refresh button
|
|
||||||
ui.refreshBtn.addEventListener('click', () => this.loadTree());
|
|
||||||
|
|
||||||
// File view buttons
|
// File view buttons
|
||||||
ui.historyBtn.addEventListener('click', () => this.showHistory());
|
ui.historyBtn.addEventListener('click', () => this.showHistory());
|
||||||
ui.diffBtn.addEventListener('click', () => this.showDiffView());
|
ui.diffBtn.addEventListener('click', () => this.showDiffView());
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,16 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CVS Proxy - Repository Browser</title>
|
<title>CVS Proxy - Repository Browser</title>
|
||||||
<link rel="stylesheet" href="styles.css">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<h1>CVS Repository Browser</h1>
|
<h1>CVS Repository Browser</h1>
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
|
<button id="themeToggleBtn" class="btn-icon" title="Toggle dark mode">🌙</button>
|
||||||
<span id="status" class="status">Connecting...</span>
|
<span id="status" class="status">Connecting...</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -21,7 +25,6 @@
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h2>Repository</h2>
|
<h2>Repository</h2>
|
||||||
<button id="refreshBtn" class="btn-icon" title="Refresh">🔄</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="treeContainer" class="tree-container">
|
<div id="treeContainer" class="tree-container">
|
||||||
<div class="loading">Loading repository...</div>
|
<div class="loading">Loading repository...</div>
|
||||||
|
|
@ -105,6 +108,7 @@
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="config.js"></script>
|
||||||
<script src="api.js"></script>
|
<script src="api.js"></script>
|
||||||
<script src="ui.js"></script>
|
<script src="ui.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,23 @@
|
||||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
--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 {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
|
|
@ -163,6 +180,16 @@ main {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tree-nested {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item-toggle {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
text-align: center;
|
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>
|
||||||
240
ui/ui.js
240
ui/ui.js
|
|
@ -8,6 +8,7 @@ class UIManager {
|
||||||
this.currentFile = null;
|
this.currentFile = null;
|
||||||
this.currentHistory = null;
|
this.currentHistory = null;
|
||||||
this.initializeElements();
|
this.initializeElements();
|
||||||
|
this.initializeTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeElements() {
|
initializeElements() {
|
||||||
|
|
@ -37,10 +38,83 @@ class UIManager {
|
||||||
|
|
||||||
// Tree elements
|
// Tree elements
|
||||||
this.treeContainer = document.getElementById('treeContainer');
|
this.treeContainer = document.getElementById('treeContainer');
|
||||||
this.refreshBtn = document.getElementById('refreshBtn');
|
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
this.status = document.getElementById('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.fileName.textContent = path.split('/').pop();
|
||||||
this.filePath.textContent = path;
|
this.filePath.textContent = path;
|
||||||
|
|
||||||
// Escape HTML and display content
|
// Detect language from file extension
|
||||||
const escapedContent = this.escapeHtml(content);
|
const extension = path.split('.').pop().toLowerCase();
|
||||||
this.fileContent.innerHTML = `<pre><code>${escapedContent}</code></pre>`;
|
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);
|
this.showView(this.fileView);
|
||||||
}
|
}
|
||||||
|
|
@ -119,24 +235,22 @@ class UIManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and colorize diff
|
// Create code element with diff language class
|
||||||
const lines = diffText.split('\n');
|
const codeElement = document.createElement('code');
|
||||||
const colorizedLines = lines.map(line => {
|
codeElement.className = 'language-diff';
|
||||||
let className = '';
|
codeElement.textContent = diffText;
|
||||||
if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@')) {
|
|
||||||
className = 'diff-line header';
|
const preElement = document.createElement('pre');
|
||||||
} else if (line.startsWith('+')) {
|
preElement.appendChild(codeElement);
|
||||||
className = 'diff-line added';
|
|
||||||
} else if (line.startsWith('-')) {
|
this.diffContent.innerHTML = '';
|
||||||
className = 'diff-line removed';
|
this.diffContent.appendChild(preElement);
|
||||||
} else {
|
|
||||||
className = 'diff-line context';
|
// Apply syntax highlighting
|
||||||
}
|
if (window.hljs) {
|
||||||
const escapedLine = this.escapeHtml(line);
|
hljs.highlightElement(codeElement);
|
||||||
return `<div class="${className}">${escapedLine}</div>`;
|
}
|
||||||
}).join('');
|
|
||||||
|
|
||||||
this.diffContent.innerHTML = `<pre><code>${colorizedLines}</code></pre>`;
|
|
||||||
this.showView(this.diffView);
|
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 {object} node - Tree node
|
||||||
* @param {string} path - Current path
|
* @param {string} path - Current path
|
||||||
* @param {HTMLElement} container - Container element
|
* @param {HTMLElement} container - Container element
|
||||||
* @param {Function} onFileClick - Callback when file is clicked
|
* @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();
|
const keys = Object.keys(node).sort();
|
||||||
|
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
|
|
@ -252,33 +367,78 @@ class UIManager {
|
||||||
|
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'tree-item';
|
item.className = 'tree-item';
|
||||||
|
item.style.paddingLeft = `${0.5 + depth * 1.25}rem`;
|
||||||
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) {
|
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', () => {
|
item.addEventListener('click', () => {
|
||||||
// Remove active class from all items
|
// Remove active class from all items
|
||||||
container.querySelectorAll('.tree-item').forEach(i => i.classList.remove('active'));
|
container.querySelectorAll('.tree-item').forEach(i => i.classList.remove('active'));
|
||||||
item.classList.add('active');
|
item.classList.add('active');
|
||||||
onFileClick(fullPath);
|
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);
|
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