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
13 changed files with 2201 additions and 84 deletions

View file

@ -2,7 +2,7 @@
A beautiful, standalone Windows desktop widget that displays your Claude.ai usage statistics in real-time.
![Claude Usage Widget](screenshot.png)
![Claude Usage Widget](assets/claude-usage-screenshot.jpg)
## Features
@ -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
@ -18,7 +20,7 @@ A beautiful, standalone Windows desktop widget that displays your Claude.ai usag
## Installation
### Download Pre-built Release
1. Download the latest `Claude-Usage-Widget-Setup.exe` from [Releases](releases)
1. Download the latest `Claude-Usage-Widget-Setup.exe` from [Releases](https://github.com/SlavomirDurej/claude-usage-widget/releases)
2. Run the installer
3. Launch "Claude Usage Widget" from Start Menu
@ -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
@ -117,8 +134,8 @@ const UPDATE_INTERVAL = 5 * 60 * 1000; // Change to your preference (in millisec
- Try re-logging in from the system tray menu
### Widget position not saving
- Position is reset on each launch
- Future version will include position memory
- Window position is now saved automatically when you drag it
- Position will be restored when you restart the app
### Build errors
```bash
@ -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
- [ ] Remember window position
- [ ] Settings panel
- [x] Remember window position
- [x] Settings panel
- [ ] Usage history graphs
- [ ] Multiple account support
- [ ] Keyboard shortcuts

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

433
main.js
View file

@ -9,14 +9,26 @@ const store = new Store({
let mainWindow = null;
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;
const WIDGET_HEIGHT = 140;
const SETTINGS_WIDTH = 500;
const SETTINGS_HEIGHT = 680;
function createMainWindow() {
mainWindow = new BrowserWindow({
// Load saved position or use defaults
const savedPosition = store.get('windowPosition');
const windowOptions = {
width: WIDGET_WIDTH,
height: WIDGET_HEIGHT,
frame: false,
@ -24,12 +36,21 @@ function createMainWindow() {
alwaysOnTop: true,
resizable: false,
skipTaskbar: false,
icon: path.join(__dirname, 'assets/icon.ico'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
};
// Apply saved position if it exists
if (savedPosition) {
windowOptions.x = savedPosition.x;
windowOptions.y = savedPosition.y;
}
mainWindow = new BrowserWindow(windowOptions);
mainWindow.loadFile('src/renderer/index.html');
@ -37,6 +58,12 @@ function createMainWindow() {
mainWindow.setAlwaysOnTop(true, 'floating');
mainWindow.setVisibleOnAllWorkspaces(true);
// Save position when window is moved
mainWindow.on('move', () => {
const position = mainWindow.getBounds();
store.set('windowPosition', { x: position.x, y: position.y });
});
mainWindow.on('closed', () => {
mainWindow = null;
});
@ -159,6 +186,260 @@ function createLoginWindow() {
});
}
// Attempt silent login in a hidden browser window
async function attemptSilentLogin() {
console.log('[Main] Attempting silent login...');
// Notify renderer that we're trying to auto-login
if (mainWindow) {
mainWindow.webContents.send('silent-login-started');
}
return new Promise((resolve) => {
silentLoginWindow = new BrowserWindow({
width: 800,
height: 700,
show: false, // Hidden window
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
});
silentLoginWindow.loadURL('https://claude.ai');
let loginCheckInterval = null;
let hasLoggedIn = false;
const SILENT_LOGIN_TIMEOUT = 15000; // 15 seconds timeout
// Function to check login status
async function checkLoginStatus() {
if (hasLoggedIn || !silentLoginWindow) return;
try {
const cookies = await session.defaultSession.cookies.get({
url: 'https://claude.ai',
name: 'sessionKey'
});
if (cookies.length > 0) {
const sessionKey = cookies[0].value;
console.log('[Main] Silent login: Session key found, attempting to get org ID...');
// Fetch org ID from API
let orgId = null;
try {
const response = await axios.get('https://claude.ai/api/organizations', {
headers: {
'Cookie': `sessionKey=${sessionKey}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
orgId = response.data[0].uuid || response.data[0].id;
console.log('[Main] Silent login: Org ID fetched from API:', orgId);
}
} catch (err) {
console.log('[Main] Silent login: API not ready yet:', err.message);
}
if (sessionKey && orgId) {
hasLoggedIn = true;
if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
console.log('[Main] Silent login successful!');
store.set('sessionKey', sessionKey);
store.set('organizationId', orgId);
if (mainWindow) {
mainWindow.webContents.send('login-success', { sessionKey, organizationId: orgId });
}
silentLoginWindow.close();
resolve(true);
}
}
} catch (error) {
console.error('[Main] Silent login check error:', error);
}
}
// Check on page load
silentLoginWindow.webContents.on('did-finish-load', async () => {
const url = silentLoginWindow.webContents.getURL();
console.log('[Main] Silent login page loaded:', url);
if (url.includes('claude.ai')) {
await checkLoginStatus();
}
});
// Also check on navigation
silentLoginWindow.webContents.on('did-navigate', async (event, url) => {
console.log('[Main] Silent login navigated to:', url);
if (url.includes('claude.ai')) {
await checkLoginStatus();
}
});
// Poll periodically
loginCheckInterval = setInterval(async () => {
if (!hasLoggedIn && silentLoginWindow) {
await checkLoginStatus();
} else if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
}, 1000);
// Timeout - if silent login doesn't work, fall back to visible login
setTimeout(() => {
if (!hasLoggedIn) {
console.log('[Main] Silent login timeout, falling back to visible login...');
if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
if (silentLoginWindow) {
silentLoginWindow.close();
}
// Notify renderer that silent login failed
if (mainWindow) {
mainWindow.webContents.send('silent-login-failed');
}
// Open visible login window
createLoginWindow();
resolve(false);
}
}, SILENT_LOGIN_TIMEOUT);
silentLoginWindow.on('closed', () => {
if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
silentLoginWindow = null;
});
});
}
// 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'));
@ -186,7 +467,7 @@ function createTray() {
{
label: 'Settings',
click: () => {
// TODO: Open settings window
createSettingsWindow();
}
},
{
@ -219,6 +500,40 @@ function createTray() {
}
}
function createSettingsWindow() {
if (settingsWindow) {
settingsWindow.focus();
return;
}
settingsWindow = new BrowserWindow({
width: SETTINGS_WIDTH,
height: SETTINGS_HEIGHT,
title: 'Settings - Claude Usage Widget',
frame: false,
resizable: true,
minimizable: true,
maximizable: false,
icon: path.join(__dirname, 'assets/icon.ico'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
settingsWindow.loadFile('src/renderer/settings.html');
settingsWindow.on('closed', () => {
settingsWindow = null;
});
// Development tools
if (process.env.NODE_ENV === 'development') {
settingsWindow.webContents.openDevTools({ mode: 'detach' });
}
}
// IPC Handlers
ipcMain.handle('get-credentials', () => {
return {
@ -255,6 +570,10 @@ ipcMain.on('open-login', () => {
createLoginWindow();
});
ipcMain.on('open-settings', () => {
createSettingsWindow();
});
ipcMain.on('minimize-window', () => {
if (mainWindow) mainWindow.hide();
});
@ -282,6 +601,31 @@ ipcMain.on('open-external', (event, url) => {
shell.openExternal(url);
});
// Color preferences handlers
const DEFAULT_COLOR_PREFERENCES = {
normal: { start: '#8b5cf6', end: '#a78bfa' },
warning: { start: '#f59e0b', end: '#fbbf24' },
danger: { start: '#ef4444', end: '#f87171' }
};
ipcMain.handle('get-color-preferences', () => {
const saved = store.get('colorPreferences');
return saved || DEFAULT_COLOR_PREFERENCES;
});
ipcMain.handle('set-color-preferences', (event, preferences) => {
store.set('colorPreferences', preferences);
return true;
});
ipcMain.handle('notify-color-change', (event, preferences) => {
// Notify main window to update colors
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('colors-changed', preferences);
}
return true;
});
ipcMain.handle('fetch-usage-data', async () => {
console.log('[Main] fetch-usage-data handler called');
const sessionKey = store.get('sessionKey');
@ -314,17 +658,89 @@ ipcMain.handle('fetch-usage-data', async () => {
if (error.response) {
console.error('[Main] Response status:', error.response.status);
if (error.response.status === 401 || error.response.status === 403) {
throw new Error('Unauthorized');
// Session expired - attempt silent re-login
console.log('[Main] Session expired, attempting silent re-login...');
store.delete('sessionKey');
store.delete('organizationId');
// Don't clear cookies - we need them for silent login to work with OAuth
// The silent login will use existing Google/OAuth session if available
// Attempt silent login (will notify renderer appropriately)
attemptSilentLogin();
throw new Error('SessionExpired');
}
}
throw error;
}
});
// 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');
@ -335,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.0.0",
"version": "1.4.0",
"description": "Desktop widget for Claude.ai usage monitoring",
"main": "main.js",
"scripts": {

View file

@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
minimizeWindow: () => ipcRenderer.send('minimize-window'),
closeWindow: () => ipcRenderer.send('close-window'),
openLogin: () => ipcRenderer.send('open-login'),
openSettings: () => ipcRenderer.send('open-settings'),
// Window position
getWindowPosition: () => ipcRenderer.invoke('get-window-position'),
@ -24,8 +25,40 @@ contextBridge.exposeInMainWorld('electronAPI', {
onRefreshUsage: (callback) => {
ipcRenderer.on('refresh-usage', () => callback());
},
onSessionExpired: (callback) => {
ipcRenderer.on('session-expired', () => callback());
},
onSilentLoginStarted: (callback) => {
ipcRenderer.on('silent-login-started', () => callback());
},
onSilentLoginFailed: (callback) => {
ipcRenderer.on('silent-login-failed', () => callback());
},
// API
fetchUsageData: () => ipcRenderer.invoke('fetch-usage-data'),
openExternal: (url) => ipcRenderer.send('open-external', url)
openExternal: (url) => ipcRenderer.send('open-external', url),
// Color preferences
getColorPreferences: () => ipcRenderer.invoke('get-color-preferences'),
setColorPreferences: (preferences) => ipcRenderer.invoke('set-color-preferences', preferences),
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

@ -9,6 +9,8 @@ const UPDATE_INTERVAL = 5 * 60 * 1000; // 5 minutes
const elements = {
loadingContainer: document.getElementById('loadingContainer'),
loginContainer: document.getElementById('loginContainer'),
noUsageContainer: document.getElementById('noUsageContainer'),
autoLoginContainer: document.getElementById('autoLoginContainer'),
mainContent: document.getElementById('mainContent'),
loginBtn: document.getElementById('loginBtn'),
refreshBtn: document.getElementById('refreshBtn'),
@ -25,11 +27,7 @@ const elements = {
weeklyTimer: document.getElementById('weeklyTimer'),
weeklyTimeText: document.getElementById('weeklyTimeText'),
settingsBtn: document.getElementById('settingsBtn'),
settingsOverlay: document.getElementById('settingsOverlay'),
closeSettingsBtn: document.getElementById('closeSettingsBtn'),
logoutBtn: document.getElementById('logoutBtn'),
coffeeBtn: document.getElementById('coffeeBtn')
settingsBtn: document.getElementById('settingsBtn')
};
// Initialize
@ -37,6 +35,14 @@ async function init() {
setupEventListeners();
credentials = await window.electronAPI.getCredentials();
// Load and apply color preferences
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();
@ -67,24 +73,9 @@ function setupEventListeners() {
window.electronAPI.closeWindow(); // Exit application completely
});
// Settings calls
// Settings button
elements.settingsBtn.addEventListener('click', () => {
elements.settingsOverlay.style.display = 'flex';
});
elements.closeSettingsBtn.addEventListener('click', () => {
elements.settingsOverlay.style.display = 'none';
});
elements.logoutBtn.addEventListener('click', async () => {
await window.electronAPI.deleteCredentials();
elements.settingsOverlay.style.display = 'none';
showLoginRequired();
window.electronAPI.openLogin();
});
elements.coffeeBtn.addEventListener('click', () => {
window.electronAPI.openExternal('https://paypal.me/SlavomirDurej?country.x=GB&locale.x=en_GB');
window.electronAPI.openSettings();
});
// Listen for login success
@ -102,6 +93,37 @@ function setupEventListeners() {
window.electronAPI.onRefreshUsage(async () => {
await fetchUsageData();
});
// Listen for session expiration events (403 errors) - only used as fallback
window.electronAPI.onSessionExpired(() => {
console.log('Session expired event received');
credentials = { sessionKey: null, organizationId: null };
showLoginRequired();
});
// Listen for silent login attempts
window.electronAPI.onSilentLoginStarted(() => {
console.log('Silent login started...');
showAutoLoginAttempt();
});
// Listen for silent login failures (falls back to visible login)
window.electronAPI.onSilentLoginFailed(() => {
console.log('Silent login failed, manual login required');
showLoginRequired();
});
// Listen for color preference changes from settings window
window.electronAPI.onColorsChanged((preferences) => {
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
@ -121,26 +143,46 @@ async function fetchUsageData() {
updateUI(data);
} catch (error) {
console.error('Error fetching usage data:', error);
if (error.message.includes('Unauthorized')) {
await window.electronAPI.deleteCredentials();
showLoginRequired();
if (error.message.includes('SessionExpired') || error.message.includes('Unauthorized')) {
// Session expired - silent login attempt is in progress
// Show auto-login UI while waiting
credentials = { sessionKey: null, organizationId: null };
showAutoLoginAttempt();
} else {
showError('Failed to fetch usage data');
}
}
}
// Update UI with usage data
// Check if there's no usage data
function hasNoUsage(data) {
const sessionUtilization = data.five_hour?.utilization || 0;
const sessionResetsAt = data.five_hour?.resets_at;
const weeklyUtilization = data.seven_day?.utilization || 0;
const weeklyResetsAt = data.seven_day?.resets_at;
return sessionUtilization === 0 && !sessionResetsAt &&
weeklyUtilization === 0 && !weeklyResetsAt;
}
// Update UI with usage 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();
return;
}
showMainContent();
refreshTimers();
startCountdown();
// Update timestamp
// Update timestamp
// const now = new Date();
// elements.lastUpdate.textContent = `Updated ${now.toLocaleTimeString()}`;
}
// Track if we've already triggered a refresh for expired timers
@ -301,12 +343,33 @@ function updateTimer(timerElement, textElement, resetsAt, totalMinutes) {
function showLoading() {
elements.loadingContainer.style.display = 'block';
elements.loginContainer.style.display = 'none';
elements.noUsageContainer.style.display = 'none';
elements.autoLoginContainer.style.display = 'none';
elements.mainContent.style.display = 'none';
}
function showLoginRequired() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'flex'; // Use flex to preserve centering
elements.noUsageContainer.style.display = 'none';
elements.autoLoginContainer.style.display = 'none';
elements.mainContent.style.display = 'none';
stopAutoUpdate();
}
function showNoUsage() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'none';
elements.noUsageContainer.style.display = 'flex';
elements.autoLoginContainer.style.display = 'none';
elements.mainContent.style.display = 'none';
}
function showAutoLoginAttempt() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'none';
elements.noUsageContainer.style.display = 'none';
elements.autoLoginContainer.style.display = 'flex';
elements.mainContent.style.display = 'none';
stopAutoUpdate();
}
@ -314,6 +377,8 @@ function showLoginRequired() {
function showMainContent() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'none';
elements.noUsageContainer.style.display = 'none';
elements.autoLoginContainer.style.display = 'none';
elements.mainContent.style.display = 'block';
}
@ -322,6 +387,53 @@ function showError(message) {
console.error(message);
}
// Color preference management
function applyColorPreferences(prefs) {
const root = document.documentElement;
// Apply normal colors
root.style.setProperty('--color-normal-start', prefs.normal.start);
root.style.setProperty('--color-normal-end', prefs.normal.end);
// Apply warning colors
root.style.setProperty('--color-warning-start', prefs.warning.start);
root.style.setProperty('--color-warning-end', prefs.warning.end);
// Apply danger colors
root.style.setProperty('--color-danger-start', prefs.danger.start);
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

@ -16,7 +16,10 @@
<div class="widget-container" id="widgetContainer">
<!-- Title Bar -->
<div class="title-bar" id="titleBar">
<div class="title">Claude Usage</div>
<div class="title">
<img src="../../assets/logo.png" alt="Logo" class="app-logo">
<span>Claude Usage</span>
</div>
<div class="controls">
<button class="control-btn settings-btn" id="settingsBtn" title="Settings">⚙️</button>
<button class="control-btn refresh-btn" id="refreshBtn" title="Refresh">
@ -53,6 +56,30 @@
</div>
</div>
<!-- No Usage State -->
<div class="no-usage-container" id="noUsageContainer" style="display: none;">
<div class="no-usage-content">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
<div class="no-usage-text-group">
<h3>No Usage Yet</h3>
<p>Start chatting with Claude to see your usage stats</p>
</div>
</div>
</div>
<!-- Auto-Login Attempt State -->
<div class="auto-login-container" id="autoLoginContainer" style="display: none;">
<div class="auto-login-content">
<div class="spinner"></div>
<div class="auto-login-text-group">
<h3>Trying to auto-login...</h3>
<p>Please wait while we reconnect</p>
</div>
</div>
</div>
<!-- Main Content -->
<div class="content" id="mainContent" style="display: none;">
<!-- Session Usage -->
@ -93,30 +120,6 @@
<span id="lastUpdate" style="display: none;"></span>
</div>
<!-- Settings Overlay -->
<div class="settings-overlay" id="settingsOverlay" style="display: none;">
<div class="settings-content">
<button class="icon-close-settings-btn" id="closeSettingsBtn" title="Close">×</button>
<p class="disclaimer">
<strong>Disclaimer:</strong> Unofficial tool not affiliated with Anthropic. Use at your own discretion.
</p>
<button class="coffee-btn" id="coffeeBtn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 8h1a4 4 0 1 1 0 8h-1"/>
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/>
<line x1="6" y1="2" x2="6" y2="4"/>
<line x1="10" y1="2" x2="10" y2="4"/>
<line x1="14" y1="2" x2="14" y2="4"/>
</svg>
If you find this useful, buy me a coffee!
</button>
<div class="settings-actions">
<button class="logout-btn" id="logoutBtn">Log Out</button>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,538 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
color: #e0e0e0;
overflow: hidden;
}
/* Title Bar */
.title-bar {
-webkit-app-region: drag;
background: rgba(0, 0, 0, 0.3);
padding: 8px 12px;
padding-left: 6px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
height: 36px;
}
.title {
color: #e0e0e0;
font-family: 'Libre Baskerville', serif;
font-size: 14px;
font-weight: 400;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 8px;
}
.app-logo {
width: 19px;
height: 19px;
border-radius: 6px;
opacity: 0.8;
}
.controls {
-webkit-app-region: no-drag;
display: flex;
gap: 8px;
margin-right: -10px;
}
.control-btn {
width: 28px;
height: 28px;
border: none;
background: rgba(255, 255, 255, 0.05);
color: #a0a0a0;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.2s ease;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.close-btn:hover {
background: #e74c3c;
color: white;
}
#settings-app {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.settings-container {
max-width: 600px;
margin: 0 auto;
padding: 24px;
flex: 1;
overflow-y: auto;
max-height: calc(100vh - 36px); /* Account for title bar height */
}
.settings-section {
margin-bottom: 24px;
}
.settings-section h2 {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: #e0e0e0;
}
/* 2-Column Grid for Color Pickers */
.color-grid-2col {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.color-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.color-item label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #a0a0a0;
text-align: center;
}
.color-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;
}
.color-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);
}
.color-item input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-item input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 6px;
}
/* Buttons */
.btn-secondary,
.btn-danger,
.btn-coffee {
width: 100%;
padding: 10px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: #e0e0e0;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-1px);
}
.btn-danger {
background: transparent;
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.5);
}
.btn-danger:hover {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.7);
}
.btn-coffee {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
color: #1a1a1a;
}
.btn-coffee:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(255, 165, 0, 0.4);
}
.btn-coffee svg {
flex-shrink: 0;
}
/* Divider */
.divider {
width: 100%;
height: 1px;
background: rgba(255, 255, 255, 0.15);
margin: 24px 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* Disclaimer */
.disclaimer {
font-size: 11px;
color: #666;
text-align: center;
line-height: 1.5;
}
.disclaimer strong {
color: #888;
}
/* Disclaimer spacing */
.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;
}

311
src/renderer/settings.html Normal file
View file

@ -0,0 +1,311 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Settings - Claude Usage Widget</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="settings-styles.css">
</head>
<body>
<div id="settings-app">
<!-- Title Bar -->
<div class="title-bar" id="titleBar">
<div class="title">
<img src="../../assets/logo.png" alt="Logo" class="app-logo">
<span>Settings</span>
</div>
<div class="controls">
<button class="control-btn minimize-btn" id="minimizeBtn" title="Minimize"></button>
<button class="control-btn close-btn" id="closeBtn" title="Close">×</button>
</div>
</div>
<div class="settings-container">
<!-- Main Window Section -->
<section class="settings-section">
<h2>Main Window</h2>
<!-- 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>
<!-- 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>
<!-- 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>Normal End</label>
<input type="color" id="colorNormalEnd" value="#a78bfa">
</div>
<div class="color-item">
<label>Warning Start</label>
<input type="color" id="colorWarningStart" value="#f59e0b">
</div>
<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>
<!-- 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>
<!-- Account Actions -->
<section class="settings-section">
<h2>Account</h2>
<button class="btn-danger" id="logoutBtn">Log Out</button>
</section>
<div class="divider"></div>
<!-- Support -->
<section class="settings-section">
<button class="btn-coffee" id="coffeeBtn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 8h1a4 4 0 1 1 0 8h-1" />
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z" />
<line x1="6" y1="2" x2="6" y2="4" />
<line x1="10" y1="2" x2="10" y2="4" />
<line x1="14" y1="2" x2="14" y2="4" />
</svg>
If you find this useful, buy me a coffee!
</button>
<!-- Disclaimer -->
<p class="disclaimer">
<strong>Disclaimer:</strong> Unofficial tool not affiliated with Anthropic. Use at your own
discretion.
</p>
</section>
</div>
</div>
<script src="settings.js"></script>
</body>
</html>

483
src/renderer/settings.js Normal file
View file

@ -0,0 +1,483 @@
// DOM elements
const elements = {
colorNormalStart: document.getElementById('colorNormalStart'),
colorNormalEnd: document.getElementById('colorNormalEnd'),
colorWarningStart: document.getElementById('colorWarningStart'),
colorWarningEnd: document.getElementById('colorWarningEnd'),
colorDangerStart: document.getElementById('colorDangerStart'),
colorDangerEnd: document.getElementById('colorDangerEnd'),
resetColorsBtn: document.getElementById('resetColorsBtn'),
logoutBtn: document.getElementById('logoutBtn'),
coffeeBtn: document.getElementById('coffeeBtn'),
minimizeBtn: document.getElementById('minimizeBtn'),
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
function setupEventListeners() {
// Color picker event listeners
const colorInputs = [
elements.colorNormalStart,
elements.colorNormalEnd,
elements.colorWarningStart,
elements.colorWarningEnd,
elements.colorDangerStart,
elements.colorDangerEnd
];
colorInputs.forEach(input => {
input.addEventListener('change', async () => {
await saveCurrentColors();
});
});
elements.resetColorsBtn.addEventListener('click', async () => {
const defaults = {
normal: { start: '#8b5cf6', end: '#a78bfa' },
warning: { start: '#f59e0b', end: '#fbbf24' },
danger: { start: '#ef4444', end: '#f87171' }
};
await window.electronAPI.setColorPreferences(defaults);
loadColorPickerValues(defaults);
// Notify main window to update colors immediately
await window.electronAPI.notifyColorChange(defaults);
});
elements.logoutBtn.addEventListener('click', async () => {
await window.electronAPI.deleteCredentials();
window.close();
window.electronAPI.openLogin();
});
elements.coffeeBtn.addEventListener('click', () => {
window.electronAPI.openExternal('https://paypal.me/SlavomirDurej?country.x=GB&locale.x=en_GB');
});
// Window controls
elements.minimizeBtn.addEventListener('click', () => {
window.electronAPI.minimizeWindow();
});
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) {
// 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 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();

View file

@ -1,3 +1,17 @@
:root {
/* Customizable color scheme - normal state */
--color-normal-start: #8b5cf6;
--color-normal-end: #a78bfa;
/* Warning state (>=75% usage) */
--color-warning-start: #f59e0b;
--color-warning-end: #fbbf24;
/* Danger state (>=90% usage) */
--color-danger-start: #ef4444;
--color-danger-end: #f87171;
}
* {
margin: 0;
padding: 0;
@ -36,6 +50,7 @@ body {
-webkit-app-region: drag;
background: rgba(0, 0, 0, 0.3);
padding: 8px 12px;
padding-left: 6px;
display: flex;
justify-content: space-between;
align-items: center;
@ -49,6 +64,16 @@ body {
font-size: 14px;
font-weight: 400;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 8px;
}
.app-logo {
width: 19px;
height: 19px;
border-radius: 6px;
opacity: 0.8;
}
.controls {
@ -177,6 +202,90 @@ body {
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
/* No Usage State */
.no-usage-container {
padding: 0 16px;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.no-usage-content {
display: flex;
align-items: center;
gap: 16px;
text-align: left;
width: 100%;
}
.no-usage-content svg {
color: #8b5cf6;
width: 32px;
height: 32px;
flex-shrink: 0;
}
.no-usage-text-group {
display: flex;
flex-direction: column;
justify-content: center;
}
.no-usage-content h3 {
color: #e0e0e0;
font-size: 14px;
margin-bottom: 2px;
}
.no-usage-content p {
color: #a0a0a0;
font-size: 11px;
margin-bottom: 0;
}
/* Auto-Login Attempt State */
.auto-login-container {
padding: 0 16px;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.auto-login-content {
display: flex;
align-items: center;
gap: 16px;
text-align: left;
width: 100%;
}
.auto-login-content .spinner {
width: 32px;
height: 32px;
flex-shrink: 0;
margin: 0;
}
.auto-login-text-group {
display: flex;
flex-direction: column;
justify-content: center;
}
.auto-login-content h3 {
color: #e0e0e0;
font-size: 14px;
margin-bottom: 2px;
}
.auto-login-content p {
color: #a0a0a0;
font-size: 11px;
margin-bottom: 0;
}
/* Main Content */
.content {
padding: 20px;
@ -244,7 +353,7 @@ body {
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #8b5cf6 0%, #a78bfa 100%);
background: linear-gradient(90deg, var(--color-normal-start) 0%, var(--color-normal-end) 100%);
border-radius: 3px;
transition: width 0.6s ease;
position: relative;
@ -254,8 +363,8 @@ body {
/* Shimmer removed */
.progress-fill.weekly {
background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%);
box-shadow: 0 0 10px rgba(59, 130, 246, 0.3);
background: linear-gradient(90deg, var(--color-normal-start) 0%, var(--color-normal-end) 100%);
box-shadow: 0 0 10px rgba(139, 92, 246, 0.3);
}
@ -283,13 +392,13 @@ body {
.timer-progress {
fill: none;
stroke: #8b5cf6;
stroke: var(--color-normal-start);
stroke-width: 4;
transition: stroke-dashoffset 0.6s ease;
}
.timer-progress.weekly {
stroke: #3b82f6;
stroke: var(--color-normal-start);
}
.timer-text {
@ -450,31 +559,31 @@ body {
}
::-webkit-scrollbar-track {
background: transparent;
background: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.25);
}
/* Warning states */
.progress-fill.warning {
background: linear-gradient(90deg, #f59e0b 0%, #fbbf24 100%);
background: linear-gradient(90deg, var(--color-warning-start) 0%, var(--color-warning-end) 100%);
}
.progress-fill.danger {
background: linear-gradient(90deg, #ef4444 0%, #f87171 100%);
background: linear-gradient(90deg, var(--color-danger-start) 0%, var(--color-danger-end) 100%);
}
.timer-progress.warning {
stroke: #f59e0b;
stroke: var(--color-warning-start);
}
.timer-progress.danger {
stroke: #ef4444;
stroke: var(--color-danger-start);
}