Compare commits
8 commits
b1fb98539f
...
325a8ed6f8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
325a8ed6f8 | ||
|
|
0e46ef084e | ||
|
|
6ea9251bfe | ||
|
|
a9a155fdd1 | ||
|
|
2cfa69cc5d | ||
|
|
5680558060 | ||
|
|
ce292d6b17 | ||
|
|
a78812d75c |
14 changed files with 1019 additions and 85 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -36,3 +36,4 @@ yarn-error.log*
|
|||
# Cache
|
||||
.cache/
|
||||
.temp/
|
||||
CLAUDE_NOTES.md
|
||||
|
|
|
|||
39
CHANGES.md
Normal file
39
CHANGES.md
Normal 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)
|
||||
35
README.md
35
README.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
A beautiful, standalone Windows desktop widget that displays your Claude.ai usage statistics in real-time.
|
||||
|
||||

|
||||

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

|
||||
|
||||
### 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 |
BIN
assets/settings-screenshot.png
Normal file
BIN
assets/settings-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
246
main.js
246
main.js
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
20
preload.js
20
preload.js
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
230
src/renderer/settings-styles.css
Normal file
230
src/renderer/settings-styles.css
Normal 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
105
src/renderer/settings.html
Normal 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
107
src/renderer/settings.js
Normal 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();
|
||||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in a new issue