From c4c758fbbd8d475d7ee69b0795ea7b715e5b36b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Dec 2025 16:09:31 -0500 Subject: [PATCH] Add dynamic tray icon with usage display and comprehensive theming system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- main.js | 187 ++++++++++++++ preload.js | 15 ++ src/icon-generator.html | 86 +++++++ src/renderer/app.js | 45 ++++ src/renderer/settings-styles.css | 308 +++++++++++++++++++++++ src/renderer/settings.html | 260 +++++++++++++++++-- src/renderer/settings.js | 418 +++++++++++++++++++++++++++++-- 7 files changed, 1271 insertions(+), 48 deletions(-) create mode 100644 src/icon-generator.html diff --git a/main.js b/main.js index ea1539c..f1d260e 100644 --- a/main.js +++ b/main.js @@ -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') { diff --git a/preload.js b/preload.js index be50ebf..5993823 100644 --- a/preload.js +++ b/preload.js @@ -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)); } }); diff --git a/src/icon-generator.html b/src/icon-generator.html new file mode 100644 index 0000000..5e69aa8 --- /dev/null +++ b/src/icon-generator.html @@ -0,0 +1,86 @@ + + + + + Icon Generator + + + + + + diff --git a/src/renderer/app.js b/src/renderer/app.js index 9c81fb2..e0a585f 100644 --- a/src/renderer/app.js +++ b/src/renderer/app.js @@ -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(); diff --git a/src/renderer/settings-styles.css b/src/renderer/settings-styles.css index defc2f5..b628c18 100644 --- a/src/renderer/settings-styles.css +++ b/src/renderer/settings-styles.css @@ -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; +} diff --git a/src/renderer/settings.html b/src/renderer/settings.html index 3e72b72..2b0e60d 100644 --- a/src/renderer/settings.html +++ b/src/renderer/settings.html @@ -27,43 +27,249 @@
- +
-

Customize Colors

+

Main Window

-
-
- - -
+ +
+ +
+ + -
- - -
+ + -
- - -
+ +
+
+ + +
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- - +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
- + +
+ +
+

Customize the widget's overall appearance

+ +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ +
+ + +
+ + +
+ + + +
+ Opacity: 30% +
+
+ +
+ + + +
+ Opacity: 10% +
+
+
+ + +
+
+
+ +
+ + +
+

Tray Icon

+ + +
+

Display Mode

+

Choose which usage metric to display in the system tray

+ +
+ + + +
+
+ + +
+

Update Interval

+

How often to refresh the tray icon (in seconds)

+ +
+ +
+ 30 seconds +
+
+
+ + +
+

Show Percentage Text

+

Display percentage number on the tray icon

+ + +
+ + +
+
+ +
+

Customize colors for the tray icon progress indicator

+ + + + + + + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+
diff --git a/src/renderer/settings.js b/src/renderer/settings.js index e48fe9b..f4b73d6 100644 --- a/src/renderer/settings.js +++ b/src/renderer/settings.js @@ -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();