Compare commits

...

8 commits

Author SHA1 Message Date
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
14 changed files with 1019 additions and 85 deletions

1
.gitignore vendored
View file

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

39
CHANGES.md Normal file
View file

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

View file

@ -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

246
main.js
View file

@ -9,14 +9,20 @@ const store = new Store({
let mainWindow = null;
let loginWindow = null;
let silentLoginWindow = null;
let settingsWindow = null;
let tray = null;
// 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 +30,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 +52,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 +180,149 @@ 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;
});
});
}
function createTray() {
try {
tray = new Tray(path.join(__dirname, 'assets/tray-icon.png'));
@ -186,7 +350,7 @@ function createTray() {
{
label: 'Settings',
click: () => {
// TODO: Open settings window
createSettingsWindow();
}
},
{
@ -219,6 +383,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 +453,10 @@ ipcMain.on('open-login', () => {
createLoginWindow();
});
ipcMain.on('open-settings', () => {
createSettingsWindow();
});
ipcMain.on('minimize-window', () => {
if (mainWindow) mainWindow.hide();
});
@ -282,6 +484,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,7 +541,18 @@ 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;

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": {
@ -15,7 +15,7 @@
"widget",
"electron"
],
"author": "Your Name",
"author": "SlavomirDurej and Seton Carmichael",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0",
@ -47,4 +47,4 @@
"!*.md"
]
}
}
}

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,25 @@ 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));
}
});

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,10 @@ async function init() {
setupEventListeners();
credentials = await window.electronAPI.getCredentials();
// Load and apply color preferences
const colorPrefs = await window.electronAPI.getColorPreferences();
applyColorPreferences(colorPrefs);
if (credentials.sessionKey && credentials.organizationId) {
showMainContent();
await fetchUsageData();
@ -67,24 +69,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 +89,31 @@ 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);
});
}
// Fetch usage data from Claude API
@ -121,26 +133,41 @@ 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;
// 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 +328,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 +362,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 +372,23 @@ 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);
}
// 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,230 @@
* {
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;
}
.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;
}

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

@ -0,0 +1,105 @@
<!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">
<!-- Color Customization -->
<section class="settings-section">
<h2>Customize Colors</h2>
<div class="color-grid-2col">
<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>
</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>

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

@ -0,0 +1,107 @@
// 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')
};
// Initialize
async function init() {
setupEventListeners();
// Load and apply color preferences
const colorPrefs = await window.electronAPI.getColorPreferences();
loadColorPickerValues(colorPrefs);
}
// 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();
});
}
// 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;
}
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
}
};
await window.electronAPI.setColorPreferences(prefs);
// Notify main window to update colors
await window.electronAPI.notifyColorChange(prefs);
}
// 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);
}