Compare commits

..

10 commits

Author SHA1 Message Date
Claude
c4c758fbbd Add dynamic tray icon with usage display and comprehensive theming system
Implemented several major enhancements to the Claude Usage Widget:

Features Added:
- Dynamic tray icon with circular progress pie chart showing usage percentage
- Configurable tray display mode (Session/Weekly) and refresh interval (10-300s, default 30s)
- Optional percentage text overlay in tray icon (default: off)
- Independent color customization for tray icon (normal/warning/danger states)
- Static color mode toggle for both main window and tray icon (single color vs gradients)
- Comprehensive theming system for main widget and settings window:
  * Background gradient customization
  * Primary and secondary text color controls
  * Title bar color with opacity slider
  * Border color with opacity slider
- Collapsible settings sections with smooth animations (minimized by default)
- Custom dark-themed scrollbar with purple accents matching app theme
- Real-time theme synchronization between main and settings windows

Technical Implementation:
- Zero-dependency tray icon generation using Electron's built-in Canvas API via hidden BrowserWindow
- IPC-based bi-directional communication for real-time theme updates
- Persistent settings storage using electron-store
- Conditional UI rendering for static vs gradient color modes
- All changes maintain upstream compatibility (no external npm dependencies added)

Files Modified:
- main.js: Added icon generator window, tray icon generation, theme IPC handlers
- preload.js: Added IPC methods for tray settings and theme management
- src/renderer/app.js: Added theme application and usage data forwarding
- src/renderer/settings.html: Added theming controls, collapsible sections, toggles
- src/renderer/settings.css: Added styles for new UI components and custom scrollbar
- src/renderer/settings.js: Added theme/tray settings logic with real-time updates

Files Added:
- src/icon-generator.html: Hidden window for canvas-based tray icon generation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-24 16:09:31 -05:00
Claude
4717458b82 Remove development artifacts for PR
- Remove CHANGES.md (development planning document)
- Revert author field to original (contributors credited via git)
- Remove personal .gitignore entry

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-24 13:51:06 -05:00
Claude
325a8ed6f8 Update README for v1.4.0 - Document settings and color customization
- Add customizable color preferences feature to Features section
- Document new Settings panel accessible from system tray
- Add comprehensive Color Preferences section with usage instructions
- Include settings window screenshot
- Update roadmap to reflect completed features (Settings panel, Custom colors)
- Bump version to 1.4.0

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-24 13:38:45 -05:00
Claude
0e46ef084e Add customizable color preferences with separate settings window
## New Features
- **Customizable Progress Bar Colors**: Users can now customize the colors for normal, warning, and danger states through a dedicated settings window
- **Separate Settings Window**: Settings now open in a frameless, resizable window (500x680px) with live preview of color changes
- **CSS Variables**: Converted hardcoded colors to CSS custom properties for dynamic theming

## Files Modified
- **main.js**: Added settings window creation, color preference storage (electron-store), and IPC handlers
- **preload.js**: Exposed color preference and settings IPC methods to renderer
- **app.js**: Implemented color preference loading and live updates from settings window
- **styles.css**: Added CSS variables for customizable colors, updated scrollbar styling
- **index.html**: Removed unused settings overlay
- **.gitignore**: Added CLAUDE_NOTES.md to prevent credential leaks

## Files Added
- **src/renderer/settings.html**: Settings window UI with 2-column color picker layout
- **src/renderer/settings.js**: Settings window logic and color management
- **src/renderer/settings-styles.css**: Settings window styling
- **CHANGES.md**: Tracking document for modifications

## Technical Details
- Color preferences stored in electron-store with encryption
- Live color updates via IPC communication between settings and main windows
- Default color scheme: Purple (normal), Orange (warning), Red (danger)
- Settings accessible via tray menu or settings button

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-24 13:38:45 -05:00
Slavomir Durej
6ea9251bfe trying to fix the log out issue 2025-12-19 16:49:02 +00:00
Slavomir Durej
a9a155fdd1 css error fix 2025-12-17 08:41:56 +00:00
Slavomir Durej
2cfa69cc5d no usage display notice implemented
remembering the last known position implemented
2025-12-17 08:41:19 +00:00
Slavomir Durej
5680558060 tweaks to logo 2025-12-16 10:48:50 +00:00
Slavomir Durej
ce292d6b17
Update link to Releases in README 2025-12-15 19:58:10 +00:00
Slavomir Durej
a78812d75c
Update screenshot path in README.md 2025-12-15 19:47:55 +00:00
12 changed files with 1293 additions and 93 deletions

1
.gitignore vendored
View file

@ -36,4 +36,3 @@ yarn-error.log*
# Cache
.cache/
.temp/
CLAUDE_NOTES.md

View file

@ -1,39 +0,0 @@
# Changes for Upstream PR
## Feature: Customizable Progress Bar Colors
### Modified Files
#### main.js
- Added `DEFAULT_COLOR_PREFERENCES` constant with default color scheme (normal, warning, danger states)
- Added IPC handler `get-color-preferences` to retrieve stored color preferences from electron-store
- Added IPC handler `set-color-preferences` to save color preferences to electron-store
- Storage schema: `colorPreferences: { normal: {start, end}, warning: {start, end}, danger: {start, end} }`
#### src/renderer/styles.css
- TODO: Add CSS custom properties (--color-normal-start, --color-normal-end, etc.)
- TODO: Convert hardcoded gradient colors to use CSS variables
- TODO: Apply to both .progress-fill and .timer-progress elements
#### src/renderer/app.js
- TODO: Add `applyColorPreferences()` function to set CSS custom properties
- TODO: Load and apply color preferences on init
- TODO: Add event listeners for color picker changes
#### src/renderer/index.html
- TODO: Add color picker UI in settings overlay (6 inputs for simplified scheme)
- TODO: Add "Reset to Defaults" button
#### preload.js
- TODO: Expose `getColorPreferences` and `setColorPreferences` IPC methods
---
## Feature: Dynamic Tray Icon with Usage Display (Planned)
- Not yet started
---
## Storage Changes
- Using existing electron-store, no additional files
- New keys: `colorPreferences`, `trayDisplayMode` (planned)

View file

@ -11,6 +11,8 @@ A beautiful, standalone Windows desktop widget that displays your Claude.ai usag
- ⏱️ **Countdown Timers** - Circular timers showing time until reset
- 🔄 **Auto-refresh** - Updates every 5 minutes automatically
- 🎨 **Modern UI** - Sleek, draggable widget with dark theme
- 🎨 **Customizable Colors** - Personalize progress bar colors for each usage level
- ⚙️ **Settings Panel** - Easy-to-use settings window for customization
- 🔒 **Secure** - Encrypted credential storage
- 📍 **Always on Top** - Stays visible across all workspaces
- 💾 **System Tray** - Minimizes to tray for easy access
@ -70,7 +72,7 @@ Right-click the tray icon for:
- Show/Hide widget
- Refresh usage data
- Re-login (if session expires)
- Settings (coming soon)
- Settings - Open customization panel
- Exit application
## Understanding the Display
@ -88,7 +90,22 @@ Right-click the tray icon for:
- **Timer** - Time remaining until weekly reset (Wednesdays 7:00 AM)
- **Same color coding** as session usage
## Configuration
## Customization
### Color Preferences
Customize the progress bar colors to match your preferences:
1. Right-click the system tray icon
2. Select "Settings"
3. Use the color pickers to customize each usage level:
- **Normal** (0-74% usage) - Default: Purple gradient
- **Warning** (75-89% usage) - Default: Orange gradient
- **Danger** (90-100% usage) - Default: Red gradient
4. Changes apply instantly to the main widget
5. Click "Reset to Defaults" to restore original colors
![Settings Window - Color Customization](assets/settings-screenshot.png)
### Auto-start on Windows Boot
@ -156,10 +173,10 @@ https://claude.ai/api/organizations/{org_id}/usage
- [ ] macOS support
- [ ] Linux support
- [ ] Custom themes
- [x] Custom color themes
- [ ] Notification alerts at usage thresholds
- [x] Remember window position
- [ ] Settings panel
- [x] Settings panel
- [ ] Usage history graphs
- [ ] Multiple account support
- [ ] Keyboard shortcuts

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

187
main.js
View file

@ -12,6 +12,12 @@ let loginWindow = null;
let silentLoginWindow = null;
let settingsWindow = null;
let tray = null;
let iconGeneratorWindow = null;
// Tray icon state
let cachedUsageData = null;
let trayUpdateInterval = null;
let lastTrayUpdate = 0;
// Window configuration
const WIDGET_WIDTH = 480;
@ -323,6 +329,117 @@ async function attemptSilentLogin() {
});
}
// Create hidden window for generating tray icons
function createIconGeneratorWindow() {
if (iconGeneratorWindow) return;
iconGeneratorWindow = new BrowserWindow({
width: 100,
height: 100,
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
iconGeneratorWindow.loadFile(path.join(__dirname, 'src/icon-generator.html'));
iconGeneratorWindow.on('closed', () => {
iconGeneratorWindow = null;
});
}
// Generate tray icon with circular progress indicator
async function generateTrayIcon(percentage, colors, showText = true) {
const { nativeImage } = require('electron');
if (!iconGeneratorWindow) {
createIconGeneratorWindow();
// Wait for window to load
await new Promise(resolve => setTimeout(resolve, 1000));
}
return new Promise((resolve) => {
ipcMain.once('icon-generated', (event, dataUrl) => {
resolve(nativeImage.createFromDataURL(dataUrl));
});
iconGeneratorWindow.webContents.send('generate-icon', { percentage, colors, showText });
});
}
// Update tray icon with current usage data
async function updateTrayIcon() {
if (!tray) return;
// If no data yet, show default icon
if (!cachedUsageData) {
tray.setImage(path.join(__dirname, 'assets/tray-icon.png'));
tray.setToolTip('Claude Usage Widget - Loading...');
return;
}
// Get tray settings from store
const traySettings = store.get('traySettings', {
displayMode: 'session',
showText: false,
colors: {
normal: { start: '#8b5cf6', end: '#a78bfa' },
warning: { start: '#f59e0b', end: '#fbbf24' },
danger: { start: '#ef4444', end: '#f87171' }
}
});
// Get appropriate usage data based on display mode
let percentage, resetsAt;
if (traySettings.displayMode === 'weekly') {
percentage = cachedUsageData.seven_day?.utilization || 0;
resetsAt = cachedUsageData.seven_day?.resets_at;
} else {
percentage = cachedUsageData.five_hour?.utilization || 0;
resetsAt = cachedUsageData.five_hour?.resets_at;
}
// Generate and set new icon
try {
const icon = await generateTrayIcon(percentage, traySettings.colors, traySettings.showText);
tray.setImage(icon);
// Update tooltip with detailed info
const modeLabel = traySettings.displayMode === 'weekly' ? '7-day' : '5-hour';
const resetTime = resetsAt ? new Date(resetsAt).toLocaleString() : 'N/A';
tray.setToolTip(
`Claude Usage: ${Math.round(percentage)}% (${modeLabel})\nResets: ${resetTime}`
);
} catch (error) {
console.error('Failed to generate tray icon:', error);
}
}
// Throttled tray update to avoid excessive updates
function updateTrayIconThrottled() {
const now = Date.now();
const interval = store.get('trayUpdateInterval', 30) * 1000; // Convert to ms
if (now - lastTrayUpdate >= interval) {
updateTrayIcon();
lastTrayUpdate = now;
}
}
// Start periodic tray icon updates
function startTrayUpdateTimer() {
if (trayUpdateInterval) clearInterval(trayUpdateInterval);
const interval = store.get('trayUpdateInterval', 30) * 1000;
trayUpdateInterval = setInterval(() => {
if (cachedUsageData) {
updateTrayIcon();
}
}, interval);
}
function createTray() {
try {
tray = new Tray(path.join(__dirname, 'assets/tray-icon.png'));
@ -559,10 +676,71 @@ ipcMain.handle('fetch-usage-data', async () => {
}
});
// Tray icon IPC handlers
ipcMain.on('usage-data-update', (event, data) => {
cachedUsageData = data;
updateTrayIconThrottled();
});
ipcMain.handle('get-tray-settings', () => {
return store.get('traySettings', {
displayMode: 'session',
showText: false,
colors: {
normal: { start: '#8b5cf6', end: '#a78bfa' },
warning: { start: '#f59e0b', end: '#fbbf24' },
danger: { start: '#ef4444', end: '#f87171' }
}
});
});
ipcMain.on('set-tray-settings', (event, settings) => {
store.set('traySettings', settings);
updateTrayIcon(); // Immediate update
});
ipcMain.handle('get-tray-update-interval', () => {
return store.get('trayUpdateInterval', 30);
});
ipcMain.on('set-tray-update-interval', (event, seconds) => {
store.set('trayUpdateInterval', seconds);
startTrayUpdateTimer(); // Restart timer with new interval
});
// Theme settings IPC handlers
ipcMain.handle('get-theme-settings', () => {
return store.get('themeSettings', {
backgroundStart: '#1e1e2e',
backgroundEnd: '#2a2a3e',
textPrimary: '#e0e0e0',
textSecondary: '#a0a0a0',
titleBarBg: '#000000',
titleBarOpacity: 30,
borderColor: '#ffffff',
borderOpacity: 10
});
});
ipcMain.handle('set-theme-settings', (event, theme) => {
store.set('themeSettings', theme);
return true;
});
ipcMain.handle('notify-theme-change', (event, theme) => {
// Notify main window to update theme
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('theme-changed', theme);
}
return true;
});
// App lifecycle
app.whenReady().then(() => {
createMainWindow();
createTray();
createIconGeneratorWindow();
startTrayUpdateTimer();
// Check if we have credentials
// const hasCredentials = store.get('sessionKey') && store.get('organizationId');
@ -573,6 +751,15 @@ app.whenReady().then(() => {
// }
});
app.on('before-quit', () => {
if (trayUpdateInterval) {
clearInterval(trayUpdateInterval);
}
if (iconGeneratorWindow && !iconGeneratorWindow.isDestroyed()) {
iconGeneratorWindow.destroy();
}
});
app.on('window-all-closed', () => {
// Don't quit on macOS
if (process.platform !== 'darwin') {

View file

@ -1,6 +1,6 @@
{
"name": "claude-usage-widget",
"version": "1.3.0",
"version": "1.4.0",
"description": "Desktop widget for Claude.ai usage monitoring",
"main": "main.js",
"scripts": {

View file

@ -45,5 +45,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
notifyColorChange: (preferences) => ipcRenderer.invoke('notify-color-change', preferences),
onColorsChanged: (callback) => {
ipcRenderer.on('colors-changed', (event, preferences) => callback(preferences));
},
// Tray icon
sendUsageToMain: (data) => ipcRenderer.send('usage-data-update', data),
getTraySettings: () => ipcRenderer.invoke('get-tray-settings'),
setTraySettings: (settings) => ipcRenderer.send('set-tray-settings', settings),
getTrayUpdateInterval: () => ipcRenderer.invoke('get-tray-update-interval'),
setTrayUpdateInterval: (seconds) => ipcRenderer.send('set-tray-update-interval', seconds),
// Theme settings
getThemeSettings: () => ipcRenderer.invoke('get-theme-settings'),
setThemeSettings: (theme) => ipcRenderer.invoke('set-theme-settings', theme),
notifyThemeChange: (theme) => ipcRenderer.invoke('notify-theme-change', theme),
onThemeChanged: (callback) => {
ipcRenderer.on('theme-changed', (event, theme) => callback(theme));
}
});

86
src/icon-generator.html Normal file
View file

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Icon Generator</title>
</head>
<body>
<canvas id="iconCanvas" width="128" height="128"></canvas>
<script>
const { ipcRenderer } = require('electron');
const canvas = document.getElementById('iconCanvas');
const ctx = canvas.getContext('2d');
ipcRenderer.on('generate-icon', (event, { percentage, colors, showText }) => {
// Clear canvas
ctx.clearRect(0, 0, 128, 128);
const centerX = 64;
const centerY = 64;
const radius = 58;
// Determine colors based on percentage
let startColor, endColor;
if (percentage >= 90) {
startColor = colors.danger.start;
endColor = colors.danger.end;
} else if (percentage >= 75) {
startColor = colors.warning.start;
endColor = colors.warning.end;
} else {
startColor = colors.normal.start;
endColor = colors.normal.end;
}
// Draw background circle (dark gray)
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.fillStyle = '#3a3a3a';
ctx.fill();
// Draw progress pie (filled wedge)
const startAngle = -Math.PI / 2; // Start at top (12 o'clock)
const endAngle = startAngle + (percentage / 100) * Math.PI * 2;
if (percentage > 0) {
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
ctx.closePath();
// Create gradient for progress pie
const gradient = ctx.createLinearGradient(0, 0, 128, 128);
gradient.addColorStop(0, startColor);
gradient.addColorStop(1, endColor);
ctx.fillStyle = gradient;
ctx.fill();
}
// Draw subtle border around the circle
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 2;
ctx.stroke();
// Draw percentage text (if enabled)
if (showText) {
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 32px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
const text = Math.round(percentage).toString() + '%';
ctx.fillText(text, centerX, centerY);
}
// Send back as data URL
ipcRenderer.send('icon-generated', canvas.toDataURL());
});
</script>
</body>
</html>

View file

@ -39,6 +39,10 @@ async function init() {
const colorPrefs = await window.electronAPI.getColorPreferences();
applyColorPreferences(colorPrefs);
// Load and apply theme settings
const themeSettings = await window.electronAPI.getThemeSettings();
applyThemePreferences(themeSettings);
if (credentials.sessionKey && credentials.organizationId) {
showMainContent();
await fetchUsageData();
@ -114,6 +118,12 @@ function setupEventListeners() {
console.log('Colors changed, applying new preferences');
applyColorPreferences(preferences);
});
// Listen for theme changes from settings window
window.electronAPI.onThemeChanged((theme) => {
console.log('Theme changed, applying new theme');
applyThemePreferences(theme);
});
}
// Fetch usage data from Claude API
@ -159,6 +169,11 @@ function hasNoUsage(data) {
function updateUI(data) {
latestUsageData = data;
// Send usage data to main process for tray icon
if (window.electronAPI && window.electronAPI.sendUsageToMain) {
window.electronAPI.sendUsageToMain(data);
}
// Check if there's no usage data
if (hasNoUsage(data)) {
showNoUsage();
@ -389,6 +404,36 @@ function applyColorPreferences(prefs) {
root.style.setProperty('--color-danger-end', prefs.danger.end);
}
// Theme preference management
function applyThemePreferences(theme) {
const root = document.documentElement;
const widgetContainer = document.querySelector('.widget-container');
const titleBar = document.querySelector('.title-bar');
// Apply background gradient
if (widgetContainer) {
widgetContainer.style.background = `linear-gradient(135deg, ${theme.backgroundStart} 0%, ${theme.backgroundEnd} 100%)`;
}
// Apply text colors
root.style.setProperty('--text-primary', theme.textPrimary);
root.style.setProperty('--text-secondary', theme.textSecondary);
// Apply title bar background
if (titleBar) {
const opacity = theme.titleBarOpacity / 100;
const bgColor = theme.titleBarBg;
titleBar.style.background = `rgba(${parseInt(bgColor.slice(1, 3), 16)}, ${parseInt(bgColor.slice(3, 5), 16)}, ${parseInt(bgColor.slice(5, 7), 16)}, ${opacity})`;
}
// Apply border color
if (widgetContainer) {
const borderOpacity = theme.borderOpacity / 100;
const borderColor = theme.borderColor;
widgetContainer.style.borderColor = `rgba(${parseInt(borderColor.slice(1, 3), 16)}, ${parseInt(borderColor.slice(3, 5), 16)}, ${parseInt(borderColor.slice(5, 7), 16)}, ${borderOpacity})`;
}
}
// Auto-update management
function startAutoUpdate() {
stopAutoUpdate();

View file

@ -86,6 +86,8 @@ body {
margin: 0 auto;
padding: 24px;
flex: 1;
overflow-y: auto;
max-height: calc(100vh - 36px); /* Account for title bar height */
}
.settings-section {
@ -228,3 +230,309 @@ body {
.disclaimer {
margin-top: 16px;
}
/* Setting Groups */
.setting-group {
margin-bottom: 25px;
}
.setting-group h3 {
font-size: 13px;
font-weight: 600;
margin-bottom: 8px;
color: #e5e7eb;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.setting-description {
color: #9ca3af;
font-size: 12px;
margin-bottom: 12px;
line-height: 1.4;
}
/* Radio Buttons */
.radio-group {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
}
.radio-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: all 0.2s;
background: rgba(255, 255, 255, 0.03);
}
.radio-item:hover {
background: rgba(139, 92, 246, 0.1);
border-color: rgba(139, 92, 246, 0.3);
}
.radio-item input[type="radio"] {
width: 18px;
height: 18px;
cursor: pointer;
margin: 0;
flex-shrink: 0;
accent-color: #8b5cf6;
}
.radio-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.radio-label {
font-weight: 500;
color: #e5e7eb;
font-size: 13px;
}
.radio-sublabel {
font-size: 11px;
color: #9ca3af;
}
/* Slider */
.slider-container {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
}
.slider-container input[type="range"] {
width: 100%;
height: 6px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.1);
outline: none;
-webkit-appearance: none;
}
.slider-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #8b5cf6;
cursor: pointer;
transition: background 0.2s;
}
.slider-container input[type="range"]::-webkit-slider-thumb:hover {
background: #a78bfa;
}
.slider-container input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #8b5cf6;
cursor: pointer;
border: none;
transition: background 0.2s;
}
.slider-container input[type="range"]::-moz-range-thumb:hover {
background: #a78bfa;
}
.slider-value {
text-align: center;
font-size: 14px;
color: #e5e7eb;
}
.slider-value span {
font-weight: 600;
color: #8b5cf6;
font-size: 16px;
}
/* Toggle Switch */
.toggle-switch {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
}
.toggle-switch input[type="checkbox"] {
display: none;
}
.toggle-slider {
position: relative;
width: 48px;
height: 24px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
transition: background 0.3s;
flex-shrink: 0;
}
.toggle-slider::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background: #9ca3af;
top: 2px;
left: 2px;
transition: all 0.3s;
}
.toggle-switch input[type="checkbox"]:checked + .toggle-slider {
background: rgba(139, 92, 246, 0.3);
}
.toggle-switch input[type="checkbox"]:checked + .toggle-slider::before {
background: #8b5cf6;
transform: translateX(24px);
}
.toggle-label {
font-size: 13px;
color: #9ca3af;
transition: color 0.3s;
}
.toggle-switch input[type="checkbox"]:checked ~ .toggle-label {
color: #8b5cf6;
}
/* Custom Scrollbar */
.settings-container::-webkit-scrollbar {
width: 8px;
}
.settings-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.settings-container::-webkit-scrollbar-thumb {
background: rgba(139, 92, 246, 0.3);
border-radius: 4px;
transition: background 0.3s;
}
.settings-container::-webkit-scrollbar-thumb:hover {
background: rgba(139, 92, 246, 0.5);
}
/* Collapsible Sections */
.collapsible-section {
margin-bottom: 16px;
}
.collapsible-header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: #e5e7eb;
font-size: 13px;
font-weight: 600;
}
.collapsible-header:hover {
background: rgba(139, 92, 246, 0.1);
border-color: rgba(139, 92, 246, 0.3);
}
.collapsible-header span {
text-transform: uppercase;
letter-spacing: 0.5px;
}
.collapsible-header .chevron {
transition: transform 0.3s ease;
color: #9ca3af;
}
.collapsible-header.active .chevron {
transform: rotate(180deg);
}
.collapsible-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
padding: 0 16px;
}
.collapsible-content.active {
max-height: 1000px;
padding: 16px;
padding-top: 16px;
}
/* Theme Grid */
.theme-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.theme-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.theme-item label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #a0a0a0;
text-align: center;
}
.theme-item input[type="color"] {
width: 80px;
height: 50px;
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
background: transparent;
cursor: pointer;
padding: 4px;
transition: all 0.2s ease;
}
.theme-item input[type="color"]:hover {
border-color: rgba(255, 255, 255, 0.4);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.theme-item input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.theme-item input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 6px;
}

View file

@ -27,43 +27,249 @@
<div class="settings-container">
<!-- Color Customization -->
<!-- Main Window Section -->
<section class="settings-section">
<h2>Customize Colors</h2>
<h2>Main Window</h2>
<div class="color-grid-2col">
<div class="color-item">
<label>Normal Start</label>
<input type="color" id="colorNormalStart" value="#8b5cf6">
</div>
<!-- Collapsible Color Customization -->
<div class="collapsible-section">
<button class="collapsible-header" id="mainWindowColorsHeader">
<span>Colors</span>
<svg class="chevron" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="collapsible-content" id="mainWindowColorsContent">
<!-- Static Color Toggle -->
<label class="toggle-switch" style="margin-bottom: 16px;">
<input type="checkbox" id="mainWindowStaticColor">
<span class="toggle-slider"></span>
<span class="toggle-label">Use Static Color</span>
</label>
<div class="color-item">
<label>Normal End</label>
<input type="color" id="colorNormalEnd" value="#a78bfa">
</div>
<!-- Static Color Picker (shown when toggle is ON) -->
<div id="mainWindowStaticColorPicker" style="display: none; margin-bottom: 16px;">
<div class="color-item" style="width: 100%;">
<label>Progress Bar Color</label>
<input type="color" id="mainWindowStaticColorValue" value="#8b5cf6">
</div>
</div>
<div class="color-item">
<label>Warning Start</label>
<input type="color" id="colorWarningStart" value="#f59e0b">
</div>
<!-- Gradient Color Pickers (shown when toggle is OFF) -->
<div class="color-grid-2col" id="mainWindowGradientPickers">
<div class="color-item">
<label>Normal Start</label>
<input type="color" id="colorNormalStart" value="#8b5cf6">
</div>
<div class="color-item">
<label>Warning End</label>
<input type="color" id="colorWarningEnd" value="#fbbf24">
</div>
<div class="color-item">
<label>Normal End</label>
<input type="color" id="colorNormalEnd" value="#a78bfa">
</div>
<div class="color-item">
<label>Danger Start</label>
<input type="color" id="colorDangerStart" value="#ef4444">
</div>
<div class="color-item">
<label>Warning Start</label>
<input type="color" id="colorWarningStart" value="#f59e0b">
</div>
<div class="color-item">
<label>Danger End</label>
<input type="color" id="colorDangerEnd" value="#f87171">
<div class="color-item">
<label>Warning End</label>
<input type="color" id="colorWarningEnd" value="#fbbf24">
</div>
<div class="color-item">
<label>Danger Start</label>
<input type="color" id="colorDangerStart" value="#ef4444">
</div>
<div class="color-item">
<label>Danger End</label>
<input type="color" id="colorDangerEnd" value="#f87171">
</div>
</div>
<button class="btn-secondary" id="resetColorsBtn">Reset to Defaults</button>
</div>
</div>
<button class="btn-secondary" id="resetColorsBtn">Reset to Defaults</button>
<!-- Theme Settings -->
<div class="collapsible-section">
<button class="collapsible-header" id="mainWindowThemeHeader">
<span>Theme</span>
<svg class="chevron" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="collapsible-content" id="mainWindowThemeContent">
<p class="setting-description">Customize the widget's overall appearance</p>
<div class="theme-grid">
<!-- Background Gradient -->
<div class="theme-item">
<label>Background Start</label>
<input type="color" id="backgroundColorStart" value="#1e1e2e">
</div>
<div class="theme-item">
<label>Background End</label>
<input type="color" id="backgroundColorEnd" value="#2a2a3e">
</div>
<!-- Text Colors -->
<div class="theme-item">
<label>Primary Text</label>
<input type="color" id="textColorPrimary" value="#e0e0e0">
</div>
<div class="theme-item">
<label>Secondary Text</label>
<input type="color" id="textColorSecondary" value="#a0a0a0">
</div>
<!-- UI Elements -->
<div class="theme-item">
<label>Title Bar Background</label>
<input type="color" id="titleBarBackground" value="#000000">
<input type="range" id="titleBarOpacity" min="0" max="100" value="30" style="width: 100%; margin-top: 4px;">
<div style="text-align: center; font-size: 10px; color: #9ca3af; margin-top: 2px;">
Opacity: <span id="titleBarOpacityValue">30</span>%
</div>
</div>
<div class="theme-item">
<label>Border Color</label>
<input type="color" id="borderColor" value="#ffffff">
<input type="range" id="borderOpacity" min="0" max="100" value="10" style="width: 100%; margin-top: 4px;">
<div style="text-align: center; font-size: 10px; color: #9ca3af; margin-top: 2px;">
Opacity: <span id="borderOpacityValue">10</span>%
</div>
</div>
</div>
<button class="btn-secondary" id="resetThemeBtn">Reset Theme</button>
</div>
</div>
</section>
<div class="divider"></div>
<!-- Tray Icon Settings -->
<section class="settings-section">
<h2>Tray Icon</h2>
<!-- Display Mode -->
<div class="setting-group">
<h3>Display Mode</h3>
<p class="setting-description">Choose which usage metric to display in the system tray</p>
<div class="radio-group">
<label class="radio-item">
<input type="radio" name="trayDisplay" value="session" id="trayDisplaySession" checked>
<div class="radio-content">
<span class="radio-label">Session Usage</span>
<span class="radio-sublabel">5-hour limit</span>
</div>
</label>
<label class="radio-item">
<input type="radio" name="trayDisplay" value="weekly" id="trayDisplayWeekly">
<div class="radio-content">
<span class="radio-label">Weekly Usage</span>
<span class="radio-sublabel">7-day limit</span>
</div>
</label>
</div>
</div>
<!-- Update Interval -->
<div class="setting-group">
<h3>Update Interval</h3>
<p class="setting-description">How often to refresh the tray icon (in seconds)</p>
<div class="slider-container">
<input type="range" id="trayUpdateInterval" min="10" max="300" value="30" step="10">
<div class="slider-value">
<span id="trayUpdateValue">30</span> seconds
</div>
</div>
</div>
<!-- Show Text Toggle -->
<div class="setting-group">
<h3>Show Percentage Text</h3>
<p class="setting-description">Display percentage number on the tray icon</p>
<label class="toggle-switch">
<input type="checkbox" id="trayShowText" checked>
<span class="toggle-slider"></span>
<span class="toggle-label" id="trayShowTextLabel">Enabled</span>
</label>
</div>
<!-- Tray Icon Colors -->
<div class="setting-group">
<div class="collapsible-section">
<button class="collapsible-header" id="trayColorsHeader">
<span>Colors</span>
<svg class="chevron" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="collapsible-content" id="trayColorsContent">
<p class="setting-description">Customize colors for the tray icon progress indicator</p>
<!-- Static Color Toggle -->
<label class="toggle-switch" style="margin-bottom: 16px;">
<input type="checkbox" id="trayStaticColor">
<span class="toggle-slider"></span>
<span class="toggle-label">Use Static Color</span>
</label>
<!-- Static Color Picker (shown when toggle is ON) -->
<div id="trayStaticColorPicker" style="display: none; margin-bottom: 16px;">
<div class="color-item" style="width: 100%;">
<label>Tray Icon Color</label>
<input type="color" id="trayStaticColorValue" value="#8b5cf6">
</div>
</div>
<!-- Gradient Color Pickers (shown when toggle is OFF) -->
<div class="color-grid-2col" id="trayGradientPickers">
<div class="color-item">
<label>Normal Start</label>
<input type="color" id="trayColorNormalStart" value="#8b5cf6">
</div>
<div class="color-item">
<label>Normal End</label>
<input type="color" id="trayColorNormalEnd" value="#a78bfa">
</div>
<div class="color-item">
<label>Warning Start</label>
<input type="color" id="trayColorWarningStart" value="#f59e0b">
</div>
<div class="color-item">
<label>Warning End</label>
<input type="color" id="trayColorWarningEnd" value="#fbbf24">
</div>
<div class="color-item">
<label>Danger Start</label>
<input type="color" id="trayColorDangerStart" value="#ef4444">
</div>
<div class="color-item">
<label>Danger End</label>
<input type="color" id="trayColorDangerEnd" value="#f87171">
</div>
</div>
<button class="btn-secondary" id="resetTrayColorsBtn">Reset Tray Colors</button>
</div>
</div>
</div>
</section>
<div class="divider"></div>

View file

@ -10,16 +10,72 @@ const elements = {
logoutBtn: document.getElementById('logoutBtn'),
coffeeBtn: document.getElementById('coffeeBtn'),
minimizeBtn: document.getElementById('minimizeBtn'),
closeBtn: document.getElementById('closeBtn')
closeBtn: document.getElementById('closeBtn'),
// Tray icon elements
trayDisplaySession: document.getElementById('trayDisplaySession'),
trayDisplayWeekly: document.getElementById('trayDisplayWeekly'),
trayUpdateInterval: document.getElementById('trayUpdateInterval'),
trayUpdateValue: document.getElementById('trayUpdateValue'),
trayShowText: document.getElementById('trayShowText'),
trayShowTextLabel: document.getElementById('trayShowTextLabel'),
trayColorNormalStart: document.getElementById('trayColorNormalStart'),
trayColorNormalEnd: document.getElementById('trayColorNormalEnd'),
trayColorWarningStart: document.getElementById('trayColorWarningStart'),
trayColorWarningEnd: document.getElementById('trayColorWarningEnd'),
trayColorDangerStart: document.getElementById('trayColorDangerStart'),
trayColorDangerEnd: document.getElementById('trayColorDangerEnd'),
resetTrayColorsBtn: document.getElementById('resetTrayColorsBtn'),
// Static color toggles
mainWindowStaticColor: document.getElementById('mainWindowStaticColor'),
mainWindowStaticColorPicker: document.getElementById('mainWindowStaticColorPicker'),
mainWindowStaticColorValue: document.getElementById('mainWindowStaticColorValue'),
mainWindowGradientPickers: document.getElementById('mainWindowGradientPickers'),
trayStaticColor: document.getElementById('trayStaticColor'),
trayStaticColorPicker: document.getElementById('trayStaticColorPicker'),
trayStaticColorValue: document.getElementById('trayStaticColorValue'),
trayGradientPickers: document.getElementById('trayGradientPickers'),
// Theme elements
backgroundColorStart: document.getElementById('backgroundColorStart'),
backgroundColorEnd: document.getElementById('backgroundColorEnd'),
textColorPrimary: document.getElementById('textColorPrimary'),
textColorSecondary: document.getElementById('textColorSecondary'),
titleBarBackground: document.getElementById('titleBarBackground'),
titleBarOpacity: document.getElementById('titleBarOpacity'),
titleBarOpacityValue: document.getElementById('titleBarOpacityValue'),
borderColor: document.getElementById('borderColor'),
borderOpacity: document.getElementById('borderOpacity'),
borderOpacityValue: document.getElementById('borderOpacityValue'),
resetThemeBtn: document.getElementById('resetThemeBtn')
};
// Initialize
async function init() {
setupEventListeners();
setupCollapsibleSections();
// Load and apply color preferences
const colorPrefs = await window.electronAPI.getColorPreferences();
loadColorPickerValues(colorPrefs);
// Load and apply tray settings
const traySettings = await window.electronAPI.getTraySettings();
loadTraySettings(traySettings);
// Load tray update interval
const interval = await window.electronAPI.getTrayUpdateInterval();
loadTrayUpdateInterval(interval);
// Load theme settings
await loadThemeSettings();
// Apply theme to settings window
const theme = await window.electronAPI.getThemeSettings();
applyThemeToSettings(theme);
// Listen for theme changes
window.electronAPI.onThemeChanged((theme) => {
applyThemeToSettings(theme);
});
}
// Event Listeners
@ -70,38 +126,358 @@ function setupEventListeners() {
elements.closeBtn.addEventListener('click', () => {
window.close();
});
// Tray icon event listeners
elements.trayDisplaySession.addEventListener('change', saveTraySettings);
elements.trayDisplayWeekly.addEventListener('change', saveTraySettings);
elements.trayShowText.addEventListener('change', () => {
elements.trayShowTextLabel.textContent = elements.trayShowText.checked ? 'Enabled' : 'Disabled';
saveTraySettings();
});
const trayColorInputs = [
elements.trayColorNormalStart,
elements.trayColorNormalEnd,
elements.trayColorWarningStart,
elements.trayColorWarningEnd,
elements.trayColorDangerStart,
elements.trayColorDangerEnd
];
trayColorInputs.forEach(input => {
input.addEventListener('change', saveTraySettings);
});
elements.trayUpdateInterval.addEventListener('input', () => {
elements.trayUpdateValue.textContent = elements.trayUpdateInterval.value;
});
elements.trayUpdateInterval.addEventListener('change', () => {
window.electronAPI.setTrayUpdateInterval(parseInt(elements.trayUpdateInterval.value));
});
elements.resetTrayColorsBtn.addEventListener('click', async () => {
const defaults = {
displayMode: elements.trayDisplaySession.checked ? 'session' : 'weekly',
showText: false,
colors: {
normal: { start: '#8b5cf6', end: '#a78bfa' },
warning: { start: '#f59e0b', end: '#fbbf24' },
danger: { start: '#ef4444', end: '#f87171' }
}
};
await window.electronAPI.setTraySettings(defaults);
loadTraySettings(defaults);
});
// Main Window static color toggle
elements.mainWindowStaticColor.addEventListener('change', () => {
const isStatic = elements.mainWindowStaticColor.checked;
elements.mainWindowStaticColorPicker.style.display = isStatic ? 'block' : 'none';
elements.mainWindowGradientPickers.style.display = isStatic ? 'none' : 'grid';
saveCurrentColors();
});
elements.mainWindowStaticColorValue.addEventListener('change', saveCurrentColors);
// Tray static color toggle
elements.trayStaticColor.addEventListener('change', () => {
const isStatic = elements.trayStaticColor.checked;
elements.trayStaticColorPicker.style.display = isStatic ? 'block' : 'none';
elements.trayGradientPickers.style.display = isStatic ? 'none' : 'grid';
saveTraySettings();
});
elements.trayStaticColorValue.addEventListener('change', saveTraySettings);
// Theme settings
const themeInputs = [
elements.backgroundColorStart,
elements.backgroundColorEnd,
elements.textColorPrimary,
elements.textColorSecondary,
elements.titleBarBackground,
elements.borderColor
];
themeInputs.forEach(input => {
input.addEventListener('change', saveThemeSettings);
});
// Opacity sliders
elements.titleBarOpacity.addEventListener('input', () => {
elements.titleBarOpacityValue.textContent = elements.titleBarOpacity.value;
});
elements.titleBarOpacity.addEventListener('change', saveThemeSettings);
elements.borderOpacity.addEventListener('input', () => {
elements.borderOpacityValue.textContent = elements.borderOpacity.value;
});
elements.borderOpacity.addEventListener('change', saveThemeSettings);
// Reset theme button
elements.resetThemeBtn.addEventListener('click', resetTheme);
}
// Helper functions
function loadColorPickerValues(prefs) {
elements.colorNormalStart.value = prefs.normal.start;
elements.colorNormalEnd.value = prefs.normal.end;
elements.colorWarningStart.value = prefs.warning.start;
elements.colorWarningEnd.value = prefs.warning.end;
elements.colorDangerStart.value = prefs.danger.start;
elements.colorDangerEnd.value = prefs.danger.end;
// Check if static mode
const isStatic = prefs.isStatic || false;
elements.mainWindowStaticColor.checked = isStatic;
if (isStatic) {
// Load static color
elements.mainWindowStaticColorValue.value = prefs.staticColor || '#8b5cf6';
elements.mainWindowStaticColorPicker.style.display = 'block';
elements.mainWindowGradientPickers.style.display = 'none';
} else {
// Load gradient colors
elements.colorNormalStart.value = prefs.normal.start;
elements.colorNormalEnd.value = prefs.normal.end;
elements.colorWarningStart.value = prefs.warning.start;
elements.colorWarningEnd.value = prefs.warning.end;
elements.colorDangerStart.value = prefs.danger.start;
elements.colorDangerEnd.value = prefs.danger.end;
elements.mainWindowStaticColorPicker.style.display = 'none';
elements.mainWindowGradientPickers.style.display = 'grid';
}
}
async function saveCurrentColors() {
const prefs = {
normal: {
start: elements.colorNormalStart.value,
end: elements.colorNormalEnd.value
},
warning: {
start: elements.colorWarningStart.value,
end: elements.colorWarningEnd.value
},
danger: {
start: elements.colorDangerStart.value,
end: elements.colorDangerEnd.value
}
};
const isStatic = elements.mainWindowStaticColor.checked;
let prefs;
if (isStatic) {
// Use static color for all states
const staticColor = elements.mainWindowStaticColorValue.value;
prefs = {
isStatic: true,
staticColor: staticColor,
normal: { start: staticColor, end: staticColor },
warning: { start: staticColor, end: staticColor },
danger: { start: staticColor, end: staticColor }
};
} else {
// Use gradient colors
prefs = {
isStatic: false,
normal: {
start: elements.colorNormalStart.value,
end: elements.colorNormalEnd.value
},
warning: {
start: elements.colorWarningStart.value,
end: elements.colorWarningEnd.value
},
danger: {
start: elements.colorDangerStart.value,
end: elements.colorDangerEnd.value
}
};
}
await window.electronAPI.setColorPreferences(prefs);
// Notify main window to update colors
await window.electronAPI.notifyColorChange(prefs);
}
// Tray settings functions
function loadTraySettings(settings) {
// Set display mode
if (settings.displayMode === 'weekly') {
elements.trayDisplayWeekly.checked = true;
} else {
elements.trayDisplaySession.checked = true;
}
// Set show text toggle
const showText = settings.showText !== undefined ? settings.showText : false;
elements.trayShowText.checked = showText;
elements.trayShowTextLabel.textContent = showText ? 'Enabled' : 'Disabled';
// Check if static mode
const isStatic = settings.isStatic || false;
elements.trayStaticColor.checked = isStatic;
if (isStatic) {
// Load static color
elements.trayStaticColorValue.value = settings.staticColor || '#8b5cf6';
elements.trayStaticColorPicker.style.display = 'block';
elements.trayGradientPickers.style.display = 'none';
} else {
// Load gradient colors
elements.trayColorNormalStart.value = settings.colors.normal.start;
elements.trayColorNormalEnd.value = settings.colors.normal.end;
elements.trayColorWarningStart.value = settings.colors.warning.start;
elements.trayColorWarningEnd.value = settings.colors.warning.end;
elements.trayColorDangerStart.value = settings.colors.danger.start;
elements.trayColorDangerEnd.value = settings.colors.danger.end;
elements.trayStaticColorPicker.style.display = 'none';
elements.trayGradientPickers.style.display = 'grid';
}
}
function loadTrayUpdateInterval(interval) {
elements.trayUpdateInterval.value = interval;
elements.trayUpdateValue.textContent = interval;
}
async function saveTraySettings() {
const isStatic = elements.trayStaticColor.checked;
let settings;
if (isStatic) {
// Use static color for all states
const staticColor = elements.trayStaticColorValue.value;
settings = {
displayMode: elements.trayDisplayWeekly.checked ? 'weekly' : 'session',
showText: elements.trayShowText.checked,
isStatic: true,
staticColor: staticColor,
colors: {
normal: { start: staticColor, end: staticColor },
warning: { start: staticColor, end: staticColor },
danger: { start: staticColor, end: staticColor }
}
};
} else {
// Use gradient colors
settings = {
displayMode: elements.trayDisplayWeekly.checked ? 'weekly' : 'session',
showText: elements.trayShowText.checked,
isStatic: false,
colors: {
normal: {
start: elements.trayColorNormalStart.value,
end: elements.trayColorNormalEnd.value
},
warning: {
start: elements.trayColorWarningStart.value,
end: elements.trayColorWarningEnd.value
},
danger: {
start: elements.trayColorDangerStart.value,
end: elements.trayColorDangerEnd.value
}
}
};
}
await window.electronAPI.setTraySettings(settings);
}
// Theme settings functions
async function loadThemeSettings() {
const theme = await window.electronAPI.getThemeSettings();
elements.backgroundColorStart.value = theme.backgroundStart || '#1e1e2e';
elements.backgroundColorEnd.value = theme.backgroundEnd || '#2a2a3e';
elements.textColorPrimary.value = theme.textPrimary || '#e0e0e0';
elements.textColorSecondary.value = theme.textSecondary || '#a0a0a0';
elements.titleBarBackground.value = theme.titleBarBg || '#000000';
elements.titleBarOpacity.value = theme.titleBarOpacity || 30;
elements.titleBarOpacityValue.textContent = elements.titleBarOpacity.value;
elements.borderColor.value = theme.borderColor || '#ffffff';
elements.borderOpacity.value = theme.borderOpacity || 10;
elements.borderOpacityValue.textContent = elements.borderOpacity.value;
}
async function saveThemeSettings() {
const theme = {
backgroundStart: elements.backgroundColorStart.value,
backgroundEnd: elements.backgroundColorEnd.value,
textPrimary: elements.textColorPrimary.value,
textSecondary: elements.textColorSecondary.value,
titleBarBg: elements.titleBarBackground.value,
titleBarOpacity: parseInt(elements.titleBarOpacity.value),
borderColor: elements.borderColor.value,
borderOpacity: parseInt(elements.borderOpacity.value)
};
await window.electronAPI.setThemeSettings(theme);
// Apply theme to settings window immediately
applyThemeToSettings(theme);
// Notify main window to update
await window.electronAPI.notifyThemeChange(theme);
}
async function resetTheme() {
const defaults = {
backgroundStart: '#1e1e2e',
backgroundEnd: '#2a2a3e',
textPrimary: '#e0e0e0',
textSecondary: '#a0a0a0',
titleBarBg: '#000000',
titleBarOpacity: 30,
borderColor: '#ffffff',
borderOpacity: 10
};
await window.electronAPI.setThemeSettings(defaults);
await loadThemeSettings();
// Apply theme to settings window immediately
applyThemeToSettings(defaults);
// Notify main window to update
await window.electronAPI.notifyThemeChange(defaults);
}
// Apply theme to settings window
function applyThemeToSettings(theme) {
const settingsApp = document.getElementById('settings-app');
const titleBar = document.querySelector('.title-bar');
// Apply background gradient
if (settingsApp) {
settingsApp.style.background = `linear-gradient(135deg, ${theme.backgroundStart} 0%, ${theme.backgroundEnd} 100%)`;
}
// Apply title bar background
if (titleBar) {
const opacity = theme.titleBarOpacity / 100;
const bgColor = theme.titleBarBg;
titleBar.style.background = `rgba(${parseInt(bgColor.slice(1, 3), 16)}, ${parseInt(bgColor.slice(3, 5), 16)}, ${parseInt(bgColor.slice(5, 7), 16)}, ${opacity})`;
}
// Apply text colors via CSS variables
const root = document.documentElement;
root.style.setProperty('--text-primary', theme.textPrimary);
root.style.setProperty('--text-secondary', theme.textSecondary);
// Update primary text color (titles, labels)
const primaryTextElements = document.querySelectorAll('.title span, h2, h3, .radio-label, .toggle-label, .setting-description');
primaryTextElements.forEach(el => {
if (el.classList.contains('setting-description')) {
// Secondary text for descriptions
el.style.color = theme.textSecondary;
} else {
// Primary text for titles/labels
el.style.color = theme.textPrimary;
}
});
}
// Setup collapsible sections
function setupCollapsibleSections() {
const collapsibleHeaders = document.querySelectorAll('.collapsible-header');
collapsibleHeaders.forEach(header => {
header.addEventListener('click', () => {
const content = header.nextElementSibling;
const isActive = header.classList.contains('active');
// Toggle active state
header.classList.toggle('active');
content.classList.toggle('active');
});
});
}
// Start the application
init();