Compare commits

..

No commits in common. "325a8ed6f8ec80549c7821d3c59bfb7573f790f1" and "b1fb98539f02f0df2c0139e8eacc8dc0307ea293" have entirely different histories.

14 changed files with 85 additions and 1019 deletions

1
.gitignore vendored
View file

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

View file

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

View file

@ -2,7 +2,7 @@
A beautiful, standalone Windows desktop widget that displays your Claude.ai usage statistics in real-time.
![Claude Usage Widget](assets/claude-usage-screenshot.jpg)
![Claude Usage Widget](screenshot.png)
## Features
@ -11,8 +11,6 @@ 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
@ -20,7 +18,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](https://github.com/SlavomirDurej/claude-usage-widget/releases)
1. Download the latest `Claude-Usage-Widget-Setup.exe` from [Releases](releases)
2. Run the installer
3. Launch "Claude Usage Widget" from Start Menu
@ -72,7 +70,7 @@ Right-click the tray icon for:
- Show/Hide widget
- Refresh usage data
- Re-login (if session expires)
- Settings - Open customization panel
- Settings (coming soon)
- Exit application
## Understanding the Display
@ -90,22 +88,7 @@ Right-click the tray icon for:
- **Timer** - Time remaining until weekly reset (Wednesdays 7:00 AM)
- **Same color coding** as session usage
## 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)
## Configuration
### Auto-start on Windows Boot
@ -134,8 +117,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
- Window position is now saved automatically when you drag it
- Position will be restored when you restart the app
- Position is reset on each launch
- Future version will include position memory
### Build errors
```bash
@ -173,10 +156,10 @@ https://claude.ai/api/organizations/{org_id}/usage
- [ ] macOS support
- [ ] Linux support
- [x] Custom color themes
- [ ] Custom themes
- [ ] Notification alerts at usage thresholds
- [x] Remember window position
- [x] Settings panel
- [ ] Remember window position
- [ ] Settings panel
- [ ] Usage history graphs
- [ ] Multiple account support
- [ ] Keyboard shortcuts

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

246
main.js
View file

@ -9,20 +9,14 @@ 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() {
// Load saved position or use defaults
const savedPosition = store.get('windowPosition');
const windowOptions = {
mainWindow = new BrowserWindow({
width: WIDGET_WIDTH,
height: WIDGET_HEIGHT,
frame: false,
@ -30,21 +24,12 @@ 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');
@ -52,12 +37,6 @@ 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;
});
@ -180,149 +159,6 @@ 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'));
@ -350,7 +186,7 @@ function createTray() {
{
label: 'Settings',
click: () => {
createSettingsWindow();
// TODO: Open settings window
}
},
{
@ -383,40 +219,6 @@ 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 {
@ -453,10 +255,6 @@ ipcMain.on('open-login', () => {
createLoginWindow();
});
ipcMain.on('open-settings', () => {
createSettingsWindow();
});
ipcMain.on('minimize-window', () => {
if (mainWindow) mainWindow.hide();
});
@ -484,31 +282,6 @@ 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');
@ -541,18 +314,7 @@ 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) {
// 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 new Error('Unauthorized');
}
}
throw error;

View file

@ -1,6 +1,6 @@
{
"name": "claude-usage-widget",
"version": "1.4.0",
"version": "1.0.0",
"description": "Desktop widget for Claude.ai usage monitoring",
"main": "main.js",
"scripts": {
@ -15,7 +15,7 @@
"widget",
"electron"
],
"author": "SlavomirDurej and Seton Carmichael",
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0",
@ -47,4 +47,4 @@
"!*.md"
]
}
}
}

View file

@ -12,7 +12,6 @@ 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'),
@ -25,25 +24,8 @@ 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),
// 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));
}
openExternal: (url) => ipcRenderer.send('open-external', url)
});

View file

@ -9,8 +9,6 @@ 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'),
@ -27,7 +25,11 @@ const elements = {
weeklyTimer: document.getElementById('weeklyTimer'),
weeklyTimeText: document.getElementById('weeklyTimeText'),
settingsBtn: document.getElementById('settingsBtn')
settingsBtn: document.getElementById('settingsBtn'),
settingsOverlay: document.getElementById('settingsOverlay'),
closeSettingsBtn: document.getElementById('closeSettingsBtn'),
logoutBtn: document.getElementById('logoutBtn'),
coffeeBtn: document.getElementById('coffeeBtn')
};
// Initialize
@ -35,10 +37,6 @@ 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();
@ -69,9 +67,24 @@ function setupEventListeners() {
window.electronAPI.closeWindow(); // Exit application completely
});
// Settings button
// Settings calls
elements.settingsBtn.addEventListener('click', () => {
window.electronAPI.openSettings();
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');
});
// Listen for login success
@ -89,31 +102,6 @@ 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
@ -133,41 +121,26 @@ async function fetchUsageData() {
updateUI(data);
} catch (error) {
console.error('Error fetching usage data:', error);
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();
if (error.message.includes('Unauthorized')) {
await window.electronAPI.deleteCredentials();
showLoginRequired();
} else {
showError('Failed to fetch 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
// 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
@ -328,33 +301,12 @@ 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();
}
@ -362,8 +314,6 @@ function showAutoLoginAttempt() {
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';
}
@ -372,23 +322,6 @@ 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,10 +16,7 @@
<div class="widget-container" id="widgetContainer">
<!-- Title Bar -->
<div class="title-bar" id="titleBar">
<div class="title">
<img src="../../assets/logo.png" alt="Logo" class="app-logo">
<span>Claude Usage</span>
</div>
<div class="title">Claude Usage</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">
@ -56,30 +53,6 @@
</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 -->
@ -120,6 +93,30 @@
<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

@ -1,230 +0,0 @@
* {
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;
}

View file

@ -1,105 +0,0 @@
<!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>

View file

@ -1,107 +0,0 @@
// 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,17 +1,3 @@
: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;
@ -50,7 +36,6 @@ 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;
@ -64,16 +49,6 @@ 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 {
@ -202,90 +177,6 @@ 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;
@ -353,7 +244,7 @@ body {
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-normal-start) 0%, var(--color-normal-end) 100%);
background: linear-gradient(90deg, #8b5cf6 0%, #a78bfa 100%);
border-radius: 3px;
transition: width 0.6s ease;
position: relative;
@ -363,8 +254,8 @@ body {
/* Shimmer removed */
.progress-fill.weekly {
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);
background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%);
box-shadow: 0 0 10px rgba(59, 130, 246, 0.3);
}
@ -392,13 +283,13 @@ body {
.timer-progress {
fill: none;
stroke: var(--color-normal-start);
stroke: #8b5cf6;
stroke-width: 4;
transition: stroke-dashoffset 0.6s ease;
}
.timer-progress.weekly {
stroke: var(--color-normal-start);
stroke: #3b82f6;
}
.timer-text {
@ -559,31 +450,31 @@ body {
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.2);
}
/* Warning states */
.progress-fill.warning {
background: linear-gradient(90deg, var(--color-warning-start) 0%, var(--color-warning-end) 100%);
background: linear-gradient(90deg, #f59e0b 0%, #fbbf24 100%);
}
.progress-fill.danger {
background: linear-gradient(90deg, var(--color-danger-start) 0%, var(--color-danger-end) 100%);
background: linear-gradient(90deg, #ef4444 0%, #f87171 100%);
}
.timer-progress.warning {
stroke: var(--color-warning-start);
stroke: #f59e0b;
}
.timer-progress.danger {
stroke: var(--color-danger-start);
stroke: #ef4444;
}