Compare commits
10 commits
5f9ffff8f6
...
c4c758fbbd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4c758fbbd | ||
|
|
4717458b82 | ||
|
|
325a8ed6f8 | ||
|
|
0e46ef084e | ||
|
|
6ea9251bfe | ||
|
|
a9a155fdd1 | ||
|
|
2cfa69cc5d | ||
|
|
5680558060 | ||
|
|
ce292d6b17 | ||
|
|
a78812d75c |
13 changed files with 2201 additions and 84 deletions
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.
|
A beautiful, standalone Windows desktop widget that displays your Claude.ai usage statistics in real-time.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## 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
|
- ⏱️ **Countdown Timers** - Circular timers showing time until reset
|
||||||
- 🔄 **Auto-refresh** - Updates every 5 minutes automatically
|
- 🔄 **Auto-refresh** - Updates every 5 minutes automatically
|
||||||
- 🎨 **Modern UI** - Sleek, draggable widget with dark theme
|
- 🎨 **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
|
- 🔒 **Secure** - Encrypted credential storage
|
||||||
- 📍 **Always on Top** - Stays visible across all workspaces
|
- 📍 **Always on Top** - Stays visible across all workspaces
|
||||||
- 💾 **System Tray** - Minimizes to tray for easy access
|
- 💾 **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
|
## Installation
|
||||||
|
|
||||||
### Download Pre-built Release
|
### 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
|
2. Run the installer
|
||||||
3. Launch "Claude Usage Widget" from Start Menu
|
3. Launch "Claude Usage Widget" from Start Menu
|
||||||
|
|
||||||
|
|
@ -70,7 +72,7 @@ Right-click the tray icon for:
|
||||||
- Show/Hide widget
|
- Show/Hide widget
|
||||||
- Refresh usage data
|
- Refresh usage data
|
||||||
- Re-login (if session expires)
|
- Re-login (if session expires)
|
||||||
- Settings (coming soon)
|
- Settings - Open customization panel
|
||||||
- Exit application
|
- Exit application
|
||||||
|
|
||||||
## Understanding the Display
|
## Understanding the Display
|
||||||
|
|
@ -88,7 +90,22 @@ Right-click the tray icon for:
|
||||||
- **Timer** - Time remaining until weekly reset (Wednesdays 7:00 AM)
|
- **Timer** - Time remaining until weekly reset (Wednesdays 7:00 AM)
|
||||||
- **Same color coding** as session usage
|
- **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
|
### 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
|
- Try re-logging in from the system tray menu
|
||||||
|
|
||||||
### Widget position not saving
|
### Widget position not saving
|
||||||
- Position is reset on each launch
|
- Window position is now saved automatically when you drag it
|
||||||
- Future version will include position memory
|
- Position will be restored when you restart the app
|
||||||
|
|
||||||
### Build errors
|
### Build errors
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -156,10 +173,10 @@ https://claude.ai/api/organizations/{org_id}/usage
|
||||||
|
|
||||||
- [ ] macOS support
|
- [ ] macOS support
|
||||||
- [ ] Linux support
|
- [ ] Linux support
|
||||||
- [ ] Custom themes
|
- [x] Custom color themes
|
||||||
- [ ] Notification alerts at usage thresholds
|
- [ ] Notification alerts at usage thresholds
|
||||||
- [ ] Remember window position
|
- [x] Remember window position
|
||||||
- [ ] Settings panel
|
- [x] Settings panel
|
||||||
- [ ] Usage history graphs
|
- [ ] Usage history graphs
|
||||||
- [ ] Multiple account support
|
- [ ] Multiple account support
|
||||||
- [ ] Keyboard shortcuts
|
- [ ] 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 |
433
main.js
433
main.js
|
|
@ -9,14 +9,26 @@ const store = new Store({
|
||||||
|
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
let loginWindow = null;
|
let loginWindow = null;
|
||||||
|
let silentLoginWindow = null;
|
||||||
|
let settingsWindow = null;
|
||||||
let tray = null;
|
let tray = null;
|
||||||
|
let iconGeneratorWindow = null;
|
||||||
|
|
||||||
|
// Tray icon state
|
||||||
|
let cachedUsageData = null;
|
||||||
|
let trayUpdateInterval = null;
|
||||||
|
let lastTrayUpdate = 0;
|
||||||
|
|
||||||
// Window configuration
|
// Window configuration
|
||||||
const WIDGET_WIDTH = 480;
|
const WIDGET_WIDTH = 480;
|
||||||
const WIDGET_HEIGHT = 140;
|
const WIDGET_HEIGHT = 140;
|
||||||
|
const SETTINGS_WIDTH = 500;
|
||||||
|
const SETTINGS_HEIGHT = 680;
|
||||||
|
|
||||||
function createMainWindow() {
|
function createMainWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
// Load saved position or use defaults
|
||||||
|
const savedPosition = store.get('windowPosition');
|
||||||
|
const windowOptions = {
|
||||||
width: WIDGET_WIDTH,
|
width: WIDGET_WIDTH,
|
||||||
height: WIDGET_HEIGHT,
|
height: WIDGET_HEIGHT,
|
||||||
frame: false,
|
frame: false,
|
||||||
|
|
@ -24,12 +36,21 @@ function createMainWindow() {
|
||||||
alwaysOnTop: true,
|
alwaysOnTop: true,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
skipTaskbar: false,
|
skipTaskbar: false,
|
||||||
|
icon: path.join(__dirname, 'assets/icon.ico'),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
preload: path.join(__dirname, 'preload.js')
|
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');
|
mainWindow.loadFile('src/renderer/index.html');
|
||||||
|
|
||||||
|
|
@ -37,6 +58,12 @@ function createMainWindow() {
|
||||||
mainWindow.setAlwaysOnTop(true, 'floating');
|
mainWindow.setAlwaysOnTop(true, 'floating');
|
||||||
mainWindow.setVisibleOnAllWorkspaces(true);
|
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.on('closed', () => {
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
@ -159,6 +186,260 @@ function createLoginWindow() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attempt silent login in a hidden browser window
|
||||||
|
async function attemptSilentLogin() {
|
||||||
|
console.log('[Main] Attempting silent login...');
|
||||||
|
|
||||||
|
// Notify renderer that we're trying to auto-login
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('silent-login-started');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
silentLoginWindow = new BrowserWindow({
|
||||||
|
width: 800,
|
||||||
|
height: 700,
|
||||||
|
show: false, // Hidden window
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
silentLoginWindow.loadURL('https://claude.ai');
|
||||||
|
|
||||||
|
let loginCheckInterval = null;
|
||||||
|
let hasLoggedIn = false;
|
||||||
|
const SILENT_LOGIN_TIMEOUT = 15000; // 15 seconds timeout
|
||||||
|
|
||||||
|
// Function to check login status
|
||||||
|
async function checkLoginStatus() {
|
||||||
|
if (hasLoggedIn || !silentLoginWindow) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cookies = await session.defaultSession.cookies.get({
|
||||||
|
url: 'https://claude.ai',
|
||||||
|
name: 'sessionKey'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cookies.length > 0) {
|
||||||
|
const sessionKey = cookies[0].value;
|
||||||
|
console.log('[Main] Silent login: Session key found, attempting to get org ID...');
|
||||||
|
|
||||||
|
// Fetch org ID from API
|
||||||
|
let orgId = null;
|
||||||
|
try {
|
||||||
|
const response = await axios.get('https://claude.ai/api/organizations', {
|
||||||
|
headers: {
|
||||||
|
'Cookie': `sessionKey=${sessionKey}`,
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||||
|
orgId = response.data[0].uuid || response.data[0].id;
|
||||||
|
console.log('[Main] Silent login: Org ID fetched from API:', orgId);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('[Main] Silent login: API not ready yet:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionKey && orgId) {
|
||||||
|
hasLoggedIn = true;
|
||||||
|
if (loginCheckInterval) {
|
||||||
|
clearInterval(loginCheckInterval);
|
||||||
|
loginCheckInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Main] Silent login successful!');
|
||||||
|
store.set('sessionKey', sessionKey);
|
||||||
|
store.set('organizationId', orgId);
|
||||||
|
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('login-success', { sessionKey, organizationId: orgId });
|
||||||
|
}
|
||||||
|
|
||||||
|
silentLoginWindow.close();
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Main] Silent login check error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check on page load
|
||||||
|
silentLoginWindow.webContents.on('did-finish-load', async () => {
|
||||||
|
const url = silentLoginWindow.webContents.getURL();
|
||||||
|
console.log('[Main] Silent login page loaded:', url);
|
||||||
|
|
||||||
|
if (url.includes('claude.ai')) {
|
||||||
|
await checkLoginStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also check on navigation
|
||||||
|
silentLoginWindow.webContents.on('did-navigate', async (event, url) => {
|
||||||
|
console.log('[Main] Silent login navigated to:', url);
|
||||||
|
if (url.includes('claude.ai')) {
|
||||||
|
await checkLoginStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Poll periodically
|
||||||
|
loginCheckInterval = setInterval(async () => {
|
||||||
|
if (!hasLoggedIn && silentLoginWindow) {
|
||||||
|
await checkLoginStatus();
|
||||||
|
} else if (loginCheckInterval) {
|
||||||
|
clearInterval(loginCheckInterval);
|
||||||
|
loginCheckInterval = null;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Timeout - if silent login doesn't work, fall back to visible login
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!hasLoggedIn) {
|
||||||
|
console.log('[Main] Silent login timeout, falling back to visible login...');
|
||||||
|
if (loginCheckInterval) {
|
||||||
|
clearInterval(loginCheckInterval);
|
||||||
|
loginCheckInterval = null;
|
||||||
|
}
|
||||||
|
if (silentLoginWindow) {
|
||||||
|
silentLoginWindow.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify renderer that silent login failed
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('silent-login-failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open visible login window
|
||||||
|
createLoginWindow();
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}, SILENT_LOGIN_TIMEOUT);
|
||||||
|
|
||||||
|
silentLoginWindow.on('closed', () => {
|
||||||
|
if (loginCheckInterval) {
|
||||||
|
clearInterval(loginCheckInterval);
|
||||||
|
loginCheckInterval = null;
|
||||||
|
}
|
||||||
|
silentLoginWindow = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create hidden window for generating tray icons
|
||||||
|
function createIconGeneratorWindow() {
|
||||||
|
if (iconGeneratorWindow) return;
|
||||||
|
|
||||||
|
iconGeneratorWindow = new BrowserWindow({
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
iconGeneratorWindow.loadFile(path.join(__dirname, 'src/icon-generator.html'));
|
||||||
|
|
||||||
|
iconGeneratorWindow.on('closed', () => {
|
||||||
|
iconGeneratorWindow = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate tray icon with circular progress indicator
|
||||||
|
async function generateTrayIcon(percentage, colors, showText = true) {
|
||||||
|
const { nativeImage } = require('electron');
|
||||||
|
|
||||||
|
if (!iconGeneratorWindow) {
|
||||||
|
createIconGeneratorWindow();
|
||||||
|
// Wait for window to load
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
ipcMain.once('icon-generated', (event, dataUrl) => {
|
||||||
|
resolve(nativeImage.createFromDataURL(dataUrl));
|
||||||
|
});
|
||||||
|
|
||||||
|
iconGeneratorWindow.webContents.send('generate-icon', { percentage, colors, showText });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tray icon with current usage data
|
||||||
|
async function updateTrayIcon() {
|
||||||
|
if (!tray) return;
|
||||||
|
|
||||||
|
// If no data yet, show default icon
|
||||||
|
if (!cachedUsageData) {
|
||||||
|
tray.setImage(path.join(__dirname, 'assets/tray-icon.png'));
|
||||||
|
tray.setToolTip('Claude Usage Widget - Loading...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get tray settings from store
|
||||||
|
const traySettings = store.get('traySettings', {
|
||||||
|
displayMode: 'session',
|
||||||
|
showText: false,
|
||||||
|
colors: {
|
||||||
|
normal: { start: '#8b5cf6', end: '#a78bfa' },
|
||||||
|
warning: { start: '#f59e0b', end: '#fbbf24' },
|
||||||
|
danger: { start: '#ef4444', end: '#f87171' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get appropriate usage data based on display mode
|
||||||
|
let percentage, resetsAt;
|
||||||
|
if (traySettings.displayMode === 'weekly') {
|
||||||
|
percentage = cachedUsageData.seven_day?.utilization || 0;
|
||||||
|
resetsAt = cachedUsageData.seven_day?.resets_at;
|
||||||
|
} else {
|
||||||
|
percentage = cachedUsageData.five_hour?.utilization || 0;
|
||||||
|
resetsAt = cachedUsageData.five_hour?.resets_at;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate and set new icon
|
||||||
|
try {
|
||||||
|
const icon = await generateTrayIcon(percentage, traySettings.colors, traySettings.showText);
|
||||||
|
tray.setImage(icon);
|
||||||
|
|
||||||
|
// Update tooltip with detailed info
|
||||||
|
const modeLabel = traySettings.displayMode === 'weekly' ? '7-day' : '5-hour';
|
||||||
|
const resetTime = resetsAt ? new Date(resetsAt).toLocaleString() : 'N/A';
|
||||||
|
tray.setToolTip(
|
||||||
|
`Claude Usage: ${Math.round(percentage)}% (${modeLabel})\nResets: ${resetTime}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate tray icon:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throttled tray update to avoid excessive updates
|
||||||
|
function updateTrayIconThrottled() {
|
||||||
|
const now = Date.now();
|
||||||
|
const interval = store.get('trayUpdateInterval', 30) * 1000; // Convert to ms
|
||||||
|
|
||||||
|
if (now - lastTrayUpdate >= interval) {
|
||||||
|
updateTrayIcon();
|
||||||
|
lastTrayUpdate = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start periodic tray icon updates
|
||||||
|
function startTrayUpdateTimer() {
|
||||||
|
if (trayUpdateInterval) clearInterval(trayUpdateInterval);
|
||||||
|
|
||||||
|
const interval = store.get('trayUpdateInterval', 30) * 1000;
|
||||||
|
trayUpdateInterval = setInterval(() => {
|
||||||
|
if (cachedUsageData) {
|
||||||
|
updateTrayIcon();
|
||||||
|
}
|
||||||
|
}, interval);
|
||||||
|
}
|
||||||
|
|
||||||
function createTray() {
|
function createTray() {
|
||||||
try {
|
try {
|
||||||
tray = new Tray(path.join(__dirname, 'assets/tray-icon.png'));
|
tray = new Tray(path.join(__dirname, 'assets/tray-icon.png'));
|
||||||
|
|
@ -186,7 +467,7 @@ function createTray() {
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
click: () => {
|
click: () => {
|
||||||
// TODO: Open settings window
|
createSettingsWindow();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -219,6 +500,40 @@ function createTray() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSettingsWindow() {
|
||||||
|
if (settingsWindow) {
|
||||||
|
settingsWindow.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsWindow = new BrowserWindow({
|
||||||
|
width: SETTINGS_WIDTH,
|
||||||
|
height: SETTINGS_HEIGHT,
|
||||||
|
title: 'Settings - Claude Usage Widget',
|
||||||
|
frame: false,
|
||||||
|
resizable: true,
|
||||||
|
minimizable: true,
|
||||||
|
maximizable: false,
|
||||||
|
icon: path.join(__dirname, 'assets/icon.ico'),
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
settingsWindow.loadFile('src/renderer/settings.html');
|
||||||
|
|
||||||
|
settingsWindow.on('closed', () => {
|
||||||
|
settingsWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Development tools
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
settingsWindow.webContents.openDevTools({ mode: 'detach' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IPC Handlers
|
// IPC Handlers
|
||||||
ipcMain.handle('get-credentials', () => {
|
ipcMain.handle('get-credentials', () => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -255,6 +570,10 @@ ipcMain.on('open-login', () => {
|
||||||
createLoginWindow();
|
createLoginWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.on('open-settings', () => {
|
||||||
|
createSettingsWindow();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.on('minimize-window', () => {
|
ipcMain.on('minimize-window', () => {
|
||||||
if (mainWindow) mainWindow.hide();
|
if (mainWindow) mainWindow.hide();
|
||||||
});
|
});
|
||||||
|
|
@ -282,6 +601,31 @@ ipcMain.on('open-external', (event, url) => {
|
||||||
shell.openExternal(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 () => {
|
ipcMain.handle('fetch-usage-data', async () => {
|
||||||
console.log('[Main] fetch-usage-data handler called');
|
console.log('[Main] fetch-usage-data handler called');
|
||||||
const sessionKey = store.get('sessionKey');
|
const sessionKey = store.get('sessionKey');
|
||||||
|
|
@ -314,17 +658,89 @@ ipcMain.handle('fetch-usage-data', async () => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
console.error('[Main] Response status:', error.response.status);
|
console.error('[Main] Response status:', error.response.status);
|
||||||
if (error.response.status === 401 || error.response.status === 403) {
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tray icon IPC handlers
|
||||||
|
ipcMain.on('usage-data-update', (event, data) => {
|
||||||
|
cachedUsageData = data;
|
||||||
|
updateTrayIconThrottled();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-tray-settings', () => {
|
||||||
|
return store.get('traySettings', {
|
||||||
|
displayMode: 'session',
|
||||||
|
showText: false,
|
||||||
|
colors: {
|
||||||
|
normal: { start: '#8b5cf6', end: '#a78bfa' },
|
||||||
|
warning: { start: '#f59e0b', end: '#fbbf24' },
|
||||||
|
danger: { start: '#ef4444', end: '#f87171' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('set-tray-settings', (event, settings) => {
|
||||||
|
store.set('traySettings', settings);
|
||||||
|
updateTrayIcon(); // Immediate update
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-tray-update-interval', () => {
|
||||||
|
return store.get('trayUpdateInterval', 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('set-tray-update-interval', (event, seconds) => {
|
||||||
|
store.set('trayUpdateInterval', seconds);
|
||||||
|
startTrayUpdateTimer(); // Restart timer with new interval
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme settings IPC handlers
|
||||||
|
ipcMain.handle('get-theme-settings', () => {
|
||||||
|
return store.get('themeSettings', {
|
||||||
|
backgroundStart: '#1e1e2e',
|
||||||
|
backgroundEnd: '#2a2a3e',
|
||||||
|
textPrimary: '#e0e0e0',
|
||||||
|
textSecondary: '#a0a0a0',
|
||||||
|
titleBarBg: '#000000',
|
||||||
|
titleBarOpacity: 30,
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
borderOpacity: 10
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('set-theme-settings', (event, theme) => {
|
||||||
|
store.set('themeSettings', theme);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('notify-theme-change', (event, theme) => {
|
||||||
|
// Notify main window to update theme
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('theme-changed', theme);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// App lifecycle
|
// App lifecycle
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
createTray();
|
createTray();
|
||||||
|
createIconGeneratorWindow();
|
||||||
|
startTrayUpdateTimer();
|
||||||
|
|
||||||
// Check if we have credentials
|
// Check if we have credentials
|
||||||
// const hasCredentials = store.get('sessionKey') && store.get('organizationId');
|
// const hasCredentials = store.get('sessionKey') && store.get('organizationId');
|
||||||
|
|
@ -335,6 +751,15 @@ app.whenReady().then(() => {
|
||||||
// }
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
if (trayUpdateInterval) {
|
||||||
|
clearInterval(trayUpdateInterval);
|
||||||
|
}
|
||||||
|
if (iconGeneratorWindow && !iconGeneratorWindow.isDestroyed()) {
|
||||||
|
iconGeneratorWindow.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
// Don't quit on macOS
|
// Don't quit on macOS
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "claude-usage-widget",
|
"name": "claude-usage-widget",
|
||||||
"version": "1.0.0",
|
"version": "1.4.0",
|
||||||
"description": "Desktop widget for Claude.ai usage monitoring",
|
"description": "Desktop widget for Claude.ai usage monitoring",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -47,4 +47,4 @@
|
||||||
"!*.md"
|
"!*.md"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
35
preload.js
35
preload.js
|
|
@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
minimizeWindow: () => ipcRenderer.send('minimize-window'),
|
minimizeWindow: () => ipcRenderer.send('minimize-window'),
|
||||||
closeWindow: () => ipcRenderer.send('close-window'),
|
closeWindow: () => ipcRenderer.send('close-window'),
|
||||||
openLogin: () => ipcRenderer.send('open-login'),
|
openLogin: () => ipcRenderer.send('open-login'),
|
||||||
|
openSettings: () => ipcRenderer.send('open-settings'),
|
||||||
|
|
||||||
// Window position
|
// Window position
|
||||||
getWindowPosition: () => ipcRenderer.invoke('get-window-position'),
|
getWindowPosition: () => ipcRenderer.invoke('get-window-position'),
|
||||||
|
|
@ -24,8 +25,40 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
onRefreshUsage: (callback) => {
|
onRefreshUsage: (callback) => {
|
||||||
ipcRenderer.on('refresh-usage', () => 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
|
// API
|
||||||
fetchUsageData: () => ipcRenderer.invoke('fetch-usage-data'),
|
fetchUsageData: () => ipcRenderer.invoke('fetch-usage-data'),
|
||||||
openExternal: (url) => ipcRenderer.send('open-external', url)
|
openExternal: (url) => ipcRenderer.send('open-external', url),
|
||||||
|
|
||||||
|
// Color preferences
|
||||||
|
getColorPreferences: () => ipcRenderer.invoke('get-color-preferences'),
|
||||||
|
setColorPreferences: (preferences) => ipcRenderer.invoke('set-color-preferences', preferences),
|
||||||
|
notifyColorChange: (preferences) => ipcRenderer.invoke('notify-color-change', preferences),
|
||||||
|
onColorsChanged: (callback) => {
|
||||||
|
ipcRenderer.on('colors-changed', (event, preferences) => callback(preferences));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tray icon
|
||||||
|
sendUsageToMain: (data) => ipcRenderer.send('usage-data-update', data),
|
||||||
|
getTraySettings: () => ipcRenderer.invoke('get-tray-settings'),
|
||||||
|
setTraySettings: (settings) => ipcRenderer.send('set-tray-settings', settings),
|
||||||
|
getTrayUpdateInterval: () => ipcRenderer.invoke('get-tray-update-interval'),
|
||||||
|
setTrayUpdateInterval: (seconds) => ipcRenderer.send('set-tray-update-interval', seconds),
|
||||||
|
|
||||||
|
// Theme settings
|
||||||
|
getThemeSettings: () => ipcRenderer.invoke('get-theme-settings'),
|
||||||
|
setThemeSettings: (theme) => ipcRenderer.invoke('set-theme-settings', theme),
|
||||||
|
notifyThemeChange: (theme) => ipcRenderer.invoke('notify-theme-change', theme),
|
||||||
|
onThemeChanged: (callback) => {
|
||||||
|
ipcRenderer.on('theme-changed', (event, theme) => callback(theme));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
86
src/icon-generator.html
Normal file
86
src/icon-generator.html
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Icon Generator</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="iconCanvas" width="128" height="128"></canvas>
|
||||||
|
<script>
|
||||||
|
const { ipcRenderer } = require('electron');
|
||||||
|
const canvas = document.getElementById('iconCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ipcRenderer.on('generate-icon', (event, { percentage, colors, showText }) => {
|
||||||
|
// Clear canvas
|
||||||
|
ctx.clearRect(0, 0, 128, 128);
|
||||||
|
|
||||||
|
const centerX = 64;
|
||||||
|
const centerY = 64;
|
||||||
|
const radius = 58;
|
||||||
|
|
||||||
|
// Determine colors based on percentage
|
||||||
|
let startColor, endColor;
|
||||||
|
if (percentage >= 90) {
|
||||||
|
startColor = colors.danger.start;
|
||||||
|
endColor = colors.danger.end;
|
||||||
|
} else if (percentage >= 75) {
|
||||||
|
startColor = colors.warning.start;
|
||||||
|
endColor = colors.warning.end;
|
||||||
|
} else {
|
||||||
|
startColor = colors.normal.start;
|
||||||
|
endColor = colors.normal.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw background circle (dark gray)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = '#3a3a3a';
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Draw progress pie (filled wedge)
|
||||||
|
const startAngle = -Math.PI / 2; // Start at top (12 o'clock)
|
||||||
|
const endAngle = startAngle + (percentage / 100) * Math.PI * 2;
|
||||||
|
|
||||||
|
if (percentage > 0) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(centerX, centerY);
|
||||||
|
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
// Create gradient for progress pie
|
||||||
|
const gradient = ctx.createLinearGradient(0, 0, 128, 128);
|
||||||
|
gradient.addColorStop(0, startColor);
|
||||||
|
gradient.addColorStop(1, endColor);
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw subtle border around the circle
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
||||||
|
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw percentage text (if enabled)
|
||||||
|
if (showText) {
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.font = 'bold 32px Arial';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
|
||||||
|
ctx.shadowBlur = 4;
|
||||||
|
ctx.shadowOffsetX = 1;
|
||||||
|
ctx.shadowOffsetY = 1;
|
||||||
|
|
||||||
|
const text = Math.round(percentage).toString() + '%';
|
||||||
|
ctx.fillText(text, centerX, centerY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send back as data URL
|
||||||
|
ipcRenderer.send('icon-generated', canvas.toDataURL());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -9,6 +9,8 @@ const UPDATE_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
const elements = {
|
const elements = {
|
||||||
loadingContainer: document.getElementById('loadingContainer'),
|
loadingContainer: document.getElementById('loadingContainer'),
|
||||||
loginContainer: document.getElementById('loginContainer'),
|
loginContainer: document.getElementById('loginContainer'),
|
||||||
|
noUsageContainer: document.getElementById('noUsageContainer'),
|
||||||
|
autoLoginContainer: document.getElementById('autoLoginContainer'),
|
||||||
mainContent: document.getElementById('mainContent'),
|
mainContent: document.getElementById('mainContent'),
|
||||||
loginBtn: document.getElementById('loginBtn'),
|
loginBtn: document.getElementById('loginBtn'),
|
||||||
refreshBtn: document.getElementById('refreshBtn'),
|
refreshBtn: document.getElementById('refreshBtn'),
|
||||||
|
|
@ -25,11 +27,7 @@ const elements = {
|
||||||
weeklyTimer: document.getElementById('weeklyTimer'),
|
weeklyTimer: document.getElementById('weeklyTimer'),
|
||||||
weeklyTimeText: document.getElementById('weeklyTimeText'),
|
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
|
// Initialize
|
||||||
|
|
@ -37,6 +35,14 @@ async function init() {
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
credentials = await window.electronAPI.getCredentials();
|
credentials = await window.electronAPI.getCredentials();
|
||||||
|
|
||||||
|
// Load and apply color preferences
|
||||||
|
const colorPrefs = await window.electronAPI.getColorPreferences();
|
||||||
|
applyColorPreferences(colorPrefs);
|
||||||
|
|
||||||
|
// Load and apply theme settings
|
||||||
|
const themeSettings = await window.electronAPI.getThemeSettings();
|
||||||
|
applyThemePreferences(themeSettings);
|
||||||
|
|
||||||
if (credentials.sessionKey && credentials.organizationId) {
|
if (credentials.sessionKey && credentials.organizationId) {
|
||||||
showMainContent();
|
showMainContent();
|
||||||
await fetchUsageData();
|
await fetchUsageData();
|
||||||
|
|
@ -67,24 +73,9 @@ function setupEventListeners() {
|
||||||
window.electronAPI.closeWindow(); // Exit application completely
|
window.electronAPI.closeWindow(); // Exit application completely
|
||||||
});
|
});
|
||||||
|
|
||||||
// Settings calls
|
// Settings button
|
||||||
elements.settingsBtn.addEventListener('click', () => {
|
elements.settingsBtn.addEventListener('click', () => {
|
||||||
elements.settingsOverlay.style.display = 'flex';
|
window.electronAPI.openSettings();
|
||||||
});
|
|
||||||
|
|
||||||
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
|
// Listen for login success
|
||||||
|
|
@ -102,6 +93,37 @@ function setupEventListeners() {
|
||||||
window.electronAPI.onRefreshUsage(async () => {
|
window.electronAPI.onRefreshUsage(async () => {
|
||||||
await fetchUsageData();
|
await fetchUsageData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for session expiration events (403 errors) - only used as fallback
|
||||||
|
window.electronAPI.onSessionExpired(() => {
|
||||||
|
console.log('Session expired event received');
|
||||||
|
credentials = { sessionKey: null, organizationId: null };
|
||||||
|
showLoginRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for silent login attempts
|
||||||
|
window.electronAPI.onSilentLoginStarted(() => {
|
||||||
|
console.log('Silent login started...');
|
||||||
|
showAutoLoginAttempt();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for silent login failures (falls back to visible login)
|
||||||
|
window.electronAPI.onSilentLoginFailed(() => {
|
||||||
|
console.log('Silent login failed, manual login required');
|
||||||
|
showLoginRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for color preference changes from settings window
|
||||||
|
window.electronAPI.onColorsChanged((preferences) => {
|
||||||
|
console.log('Colors changed, applying new preferences');
|
||||||
|
applyColorPreferences(preferences);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for theme changes from settings window
|
||||||
|
window.electronAPI.onThemeChanged((theme) => {
|
||||||
|
console.log('Theme changed, applying new theme');
|
||||||
|
applyThemePreferences(theme);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch usage data from Claude API
|
// Fetch usage data from Claude API
|
||||||
|
|
@ -121,26 +143,46 @@ async function fetchUsageData() {
|
||||||
updateUI(data);
|
updateUI(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching usage data:', error);
|
console.error('Error fetching usage data:', error);
|
||||||
if (error.message.includes('Unauthorized')) {
|
if (error.message.includes('SessionExpired') || error.message.includes('Unauthorized')) {
|
||||||
await window.electronAPI.deleteCredentials();
|
// Session expired - silent login attempt is in progress
|
||||||
showLoginRequired();
|
// Show auto-login UI while waiting
|
||||||
|
credentials = { sessionKey: null, organizationId: null };
|
||||||
|
showAutoLoginAttempt();
|
||||||
} else {
|
} else {
|
||||||
showError('Failed to fetch usage data');
|
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
|
// Update UI with usage data
|
||||||
function updateUI(data) {
|
function updateUI(data) {
|
||||||
latestUsageData = data;
|
latestUsageData = data;
|
||||||
|
|
||||||
|
// Send usage data to main process for tray icon
|
||||||
|
if (window.electronAPI && window.electronAPI.sendUsageToMain) {
|
||||||
|
window.electronAPI.sendUsageToMain(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's no usage data
|
||||||
|
if (hasNoUsage(data)) {
|
||||||
|
showNoUsage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showMainContent();
|
||||||
refreshTimers();
|
refreshTimers();
|
||||||
startCountdown();
|
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
|
// Track if we've already triggered a refresh for expired timers
|
||||||
|
|
@ -301,12 +343,33 @@ function updateTimer(timerElement, textElement, resetsAt, totalMinutes) {
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
elements.loadingContainer.style.display = 'block';
|
elements.loadingContainer.style.display = 'block';
|
||||||
elements.loginContainer.style.display = 'none';
|
elements.loginContainer.style.display = 'none';
|
||||||
|
elements.noUsageContainer.style.display = 'none';
|
||||||
|
elements.autoLoginContainer.style.display = 'none';
|
||||||
elements.mainContent.style.display = 'none';
|
elements.mainContent.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLoginRequired() {
|
function showLoginRequired() {
|
||||||
elements.loadingContainer.style.display = 'none';
|
elements.loadingContainer.style.display = 'none';
|
||||||
elements.loginContainer.style.display = 'flex'; // Use flex to preserve centering
|
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';
|
elements.mainContent.style.display = 'none';
|
||||||
stopAutoUpdate();
|
stopAutoUpdate();
|
||||||
}
|
}
|
||||||
|
|
@ -314,6 +377,8 @@ function showLoginRequired() {
|
||||||
function showMainContent() {
|
function showMainContent() {
|
||||||
elements.loadingContainer.style.display = 'none';
|
elements.loadingContainer.style.display = 'none';
|
||||||
elements.loginContainer.style.display = 'none';
|
elements.loginContainer.style.display = 'none';
|
||||||
|
elements.noUsageContainer.style.display = 'none';
|
||||||
|
elements.autoLoginContainer.style.display = 'none';
|
||||||
elements.mainContent.style.display = 'block';
|
elements.mainContent.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,6 +387,53 @@ function showError(message) {
|
||||||
console.error(message);
|
console.error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Color preference management
|
||||||
|
function applyColorPreferences(prefs) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
// Apply normal colors
|
||||||
|
root.style.setProperty('--color-normal-start', prefs.normal.start);
|
||||||
|
root.style.setProperty('--color-normal-end', prefs.normal.end);
|
||||||
|
|
||||||
|
// Apply warning colors
|
||||||
|
root.style.setProperty('--color-warning-start', prefs.warning.start);
|
||||||
|
root.style.setProperty('--color-warning-end', prefs.warning.end);
|
||||||
|
|
||||||
|
// Apply danger colors
|
||||||
|
root.style.setProperty('--color-danger-start', prefs.danger.start);
|
||||||
|
root.style.setProperty('--color-danger-end', prefs.danger.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme preference management
|
||||||
|
function applyThemePreferences(theme) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const widgetContainer = document.querySelector('.widget-container');
|
||||||
|
const titleBar = document.querySelector('.title-bar');
|
||||||
|
|
||||||
|
// Apply background gradient
|
||||||
|
if (widgetContainer) {
|
||||||
|
widgetContainer.style.background = `linear-gradient(135deg, ${theme.backgroundStart} 0%, ${theme.backgroundEnd} 100%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply text colors
|
||||||
|
root.style.setProperty('--text-primary', theme.textPrimary);
|
||||||
|
root.style.setProperty('--text-secondary', theme.textSecondary);
|
||||||
|
|
||||||
|
// Apply title bar background
|
||||||
|
if (titleBar) {
|
||||||
|
const opacity = theme.titleBarOpacity / 100;
|
||||||
|
const bgColor = theme.titleBarBg;
|
||||||
|
titleBar.style.background = `rgba(${parseInt(bgColor.slice(1, 3), 16)}, ${parseInt(bgColor.slice(3, 5), 16)}, ${parseInt(bgColor.slice(5, 7), 16)}, ${opacity})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply border color
|
||||||
|
if (widgetContainer) {
|
||||||
|
const borderOpacity = theme.borderOpacity / 100;
|
||||||
|
const borderColor = theme.borderColor;
|
||||||
|
widgetContainer.style.borderColor = `rgba(${parseInt(borderColor.slice(1, 3), 16)}, ${parseInt(borderColor.slice(3, 5), 16)}, ${parseInt(borderColor.slice(5, 7), 16)}, ${borderOpacity})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-update management
|
// Auto-update management
|
||||||
function startAutoUpdate() {
|
function startAutoUpdate() {
|
||||||
stopAutoUpdate();
|
stopAutoUpdate();
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,10 @@
|
||||||
<div class="widget-container" id="widgetContainer">
|
<div class="widget-container" id="widgetContainer">
|
||||||
<!-- Title Bar -->
|
<!-- Title Bar -->
|
||||||
<div class="title-bar" id="titleBar">
|
<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">
|
<div class="controls">
|
||||||
<button class="control-btn settings-btn" id="settingsBtn" title="Settings">⚙️</button>
|
<button class="control-btn settings-btn" id="settingsBtn" title="Settings">⚙️</button>
|
||||||
<button class="control-btn refresh-btn" id="refreshBtn" title="Refresh">
|
<button class="control-btn refresh-btn" id="refreshBtn" title="Refresh">
|
||||||
|
|
@ -53,6 +56,30 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Main Content -->
|
||||||
<div class="content" id="mainContent" style="display: none;">
|
<div class="content" id="mainContent" style="display: none;">
|
||||||
<!-- Session Usage -->
|
<!-- Session Usage -->
|
||||||
|
|
@ -93,30 +120,6 @@
|
||||||
<span id="lastUpdate" style="display: none;"></span>
|
<span id="lastUpdate" style="display: none;"></span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
538
src/renderer/settings-styles.css
Normal file
538
src/renderer/settings-styles.css
Normal file
|
|
@ -0,0 +1,538 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
|
||||||
|
color: #e0e0e0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title Bar */
|
||||||
|
.title-bar {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 8px 12px;
|
||||||
|
padding-left: 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: 'Libre Baskerville', serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-logo {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
border-radius: 6px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-right: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #a0a0a0;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings-app {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(100vh - 36px); /* Account for title bar height */
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2-Column Grid for Color Pickers */
|
||||||
|
.color-grid-2col {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item input[type="color"] {
|
||||||
|
width: 80px;
|
||||||
|
height: 50px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item input[type="color"]:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item input[type="color"]::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item input[type="color"]::-webkit-color-swatch {
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn-secondary,
|
||||||
|
.btn-danger,
|
||||||
|
.btn-coffee {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: transparent;
|
||||||
|
color: #ef4444;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
border-color: rgba(239, 68, 68, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-coffee {
|
||||||
|
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-coffee:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(255, 165, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-coffee svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Divider */
|
||||||
|
.divider {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
margin: 24px 0;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disclaimer */
|
||||||
|
.disclaimer {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclaimer strong {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disclaimer spacing */
|
||||||
|
.disclaimer {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Setting Groups */
|
||||||
|
.setting-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group h3 {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #e5e7eb;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-description {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Radio Buttons */
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
border-color: rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item input[type="radio"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
accent-color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-sublabel {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slider */
|
||||||
|
.slider-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #8b5cf6;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container input[type="range"]::-webkit-slider-thumb:hover {
|
||||||
|
background: #a78bfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #8b5cf6;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container input[type="range"]::-moz-range-thumb:hover {
|
||||||
|
background: #a78bfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-value {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-value span {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8b5cf6;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Switch */
|
||||||
|
.toggle-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: relative;
|
||||||
|
width: 48px;
|
||||||
|
height: 24px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #9ca3af;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input[type="checkbox"]:checked + .toggle-slider {
|
||||||
|
background: rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input[type="checkbox"]:checked + .toggle-slider::before {
|
||||||
|
background: #8b5cf6;
|
||||||
|
transform: translateX(24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input[type="checkbox"]:checked ~ .toggle-label {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
.settings-container::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(139, 92, 246, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsible Sections */
|
||||||
|
.collapsible-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
border-color: rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header span {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header .chevron {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header.active .chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content.active {
|
||||||
|
max-height: 1000px;
|
||||||
|
padding: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Grid */
|
||||||
|
.theme-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-item label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-item input[type="color"] {
|
||||||
|
width: 80px;
|
||||||
|
height: 50px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-item input[type="color"]:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-item input[type="color"]::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-item input[type="color"]::-webkit-color-swatch {
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
311
src/renderer/settings.html
Normal file
311
src/renderer/settings.html
Normal file
|
|
@ -0,0 +1,311 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Settings - Claude Usage Widget</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="settings-styles.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="settings-app">
|
||||||
|
<!-- Title Bar -->
|
||||||
|
<div class="title-bar" id="titleBar">
|
||||||
|
<div class="title">
|
||||||
|
<img src="../../assets/logo.png" alt="Logo" class="app-logo">
|
||||||
|
<span>Settings</span>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="control-btn minimize-btn" id="minimizeBtn" title="Minimize">−</button>
|
||||||
|
<button class="control-btn close-btn" id="closeBtn" title="Close">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-container">
|
||||||
|
|
||||||
|
<!-- Main Window Section -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>Main Window</h2>
|
||||||
|
|
||||||
|
<!-- Collapsible Color Customization -->
|
||||||
|
<div class="collapsible-section">
|
||||||
|
<button class="collapsible-header" id="mainWindowColorsHeader">
|
||||||
|
<span>Colors</span>
|
||||||
|
<svg class="chevron" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="collapsible-content" id="mainWindowColorsContent">
|
||||||
|
<!-- Static Color Toggle -->
|
||||||
|
<label class="toggle-switch" style="margin-bottom: 16px;">
|
||||||
|
<input type="checkbox" id="mainWindowStaticColor">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
<span class="toggle-label">Use Static Color</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Static Color Picker (shown when toggle is ON) -->
|
||||||
|
<div id="mainWindowStaticColorPicker" style="display: none; margin-bottom: 16px;">
|
||||||
|
<div class="color-item" style="width: 100%;">
|
||||||
|
<label>Progress Bar Color</label>
|
||||||
|
<input type="color" id="mainWindowStaticColorValue" value="#8b5cf6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gradient Color Pickers (shown when toggle is OFF) -->
|
||||||
|
<div class="color-grid-2col" id="mainWindowGradientPickers">
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Normal Start</label>
|
||||||
|
<input type="color" id="colorNormalStart" value="#8b5cf6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Normal End</label>
|
||||||
|
<input type="color" id="colorNormalEnd" value="#a78bfa">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Warning Start</label>
|
||||||
|
<input type="color" id="colorWarningStart" value="#f59e0b">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Warning End</label>
|
||||||
|
<input type="color" id="colorWarningEnd" value="#fbbf24">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Danger Start</label>
|
||||||
|
<input type="color" id="colorDangerStart" value="#ef4444">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Danger End</label>
|
||||||
|
<input type="color" id="colorDangerEnd" value="#f87171">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn-secondary" id="resetColorsBtn">Reset to Defaults</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Theme Settings -->
|
||||||
|
<div class="collapsible-section">
|
||||||
|
<button class="collapsible-header" id="mainWindowThemeHeader">
|
||||||
|
<span>Theme</span>
|
||||||
|
<svg class="chevron" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="collapsible-content" id="mainWindowThemeContent">
|
||||||
|
<p class="setting-description">Customize the widget's overall appearance</p>
|
||||||
|
|
||||||
|
<div class="theme-grid">
|
||||||
|
<!-- Background Gradient -->
|
||||||
|
<div class="theme-item">
|
||||||
|
<label>Background Start</label>
|
||||||
|
<input type="color" id="backgroundColorStart" value="#1e1e2e">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="theme-item">
|
||||||
|
<label>Background End</label>
|
||||||
|
<input type="color" id="backgroundColorEnd" value="#2a2a3e">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text Colors -->
|
||||||
|
<div class="theme-item">
|
||||||
|
<label>Primary Text</label>
|
||||||
|
<input type="color" id="textColorPrimary" value="#e0e0e0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="theme-item">
|
||||||
|
<label>Secondary Text</label>
|
||||||
|
<input type="color" id="textColorSecondary" value="#a0a0a0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UI Elements -->
|
||||||
|
<div class="theme-item">
|
||||||
|
<label>Title Bar Background</label>
|
||||||
|
<input type="color" id="titleBarBackground" value="#000000">
|
||||||
|
<input type="range" id="titleBarOpacity" min="0" max="100" value="30" style="width: 100%; margin-top: 4px;">
|
||||||
|
<div style="text-align: center; font-size: 10px; color: #9ca3af; margin-top: 2px;">
|
||||||
|
Opacity: <span id="titleBarOpacityValue">30</span>%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="theme-item">
|
||||||
|
<label>Border Color</label>
|
||||||
|
<input type="color" id="borderColor" value="#ffffff">
|
||||||
|
<input type="range" id="borderOpacity" min="0" max="100" value="10" style="width: 100%; margin-top: 4px;">
|
||||||
|
<div style="text-align: center; font-size: 10px; color: #9ca3af; margin-top: 2px;">
|
||||||
|
Opacity: <span id="borderOpacityValue">10</span>%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn-secondary" id="resetThemeBtn">Reset Theme</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Tray Icon Settings -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>Tray Icon</h2>
|
||||||
|
|
||||||
|
<!-- Display Mode -->
|
||||||
|
<div class="setting-group">
|
||||||
|
<h3>Display Mode</h3>
|
||||||
|
<p class="setting-description">Choose which usage metric to display in the system tray</p>
|
||||||
|
|
||||||
|
<div class="radio-group">
|
||||||
|
<label class="radio-item">
|
||||||
|
<input type="radio" name="trayDisplay" value="session" id="trayDisplaySession" checked>
|
||||||
|
<div class="radio-content">
|
||||||
|
<span class="radio-label">Session Usage</span>
|
||||||
|
<span class="radio-sublabel">5-hour limit</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="radio-item">
|
||||||
|
<input type="radio" name="trayDisplay" value="weekly" id="trayDisplayWeekly">
|
||||||
|
<div class="radio-content">
|
||||||
|
<span class="radio-label">Weekly Usage</span>
|
||||||
|
<span class="radio-sublabel">7-day limit</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Update Interval -->
|
||||||
|
<div class="setting-group">
|
||||||
|
<h3>Update Interval</h3>
|
||||||
|
<p class="setting-description">How often to refresh the tray icon (in seconds)</p>
|
||||||
|
|
||||||
|
<div class="slider-container">
|
||||||
|
<input type="range" id="trayUpdateInterval" min="10" max="300" value="30" step="10">
|
||||||
|
<div class="slider-value">
|
||||||
|
<span id="trayUpdateValue">30</span> seconds
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show Text Toggle -->
|
||||||
|
<div class="setting-group">
|
||||||
|
<h3>Show Percentage Text</h3>
|
||||||
|
<p class="setting-description">Display percentage number on the tray icon</p>
|
||||||
|
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="trayShowText" checked>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
<span class="toggle-label" id="trayShowTextLabel">Enabled</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tray Icon Colors -->
|
||||||
|
<div class="setting-group">
|
||||||
|
<div class="collapsible-section">
|
||||||
|
<button class="collapsible-header" id="trayColorsHeader">
|
||||||
|
<span>Colors</span>
|
||||||
|
<svg class="chevron" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="collapsible-content" id="trayColorsContent">
|
||||||
|
<p class="setting-description">Customize colors for the tray icon progress indicator</p>
|
||||||
|
|
||||||
|
<!-- Static Color Toggle -->
|
||||||
|
<label class="toggle-switch" style="margin-bottom: 16px;">
|
||||||
|
<input type="checkbox" id="trayStaticColor">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
<span class="toggle-label">Use Static Color</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Static Color Picker (shown when toggle is ON) -->
|
||||||
|
<div id="trayStaticColorPicker" style="display: none; margin-bottom: 16px;">
|
||||||
|
<div class="color-item" style="width: 100%;">
|
||||||
|
<label>Tray Icon Color</label>
|
||||||
|
<input type="color" id="trayStaticColorValue" value="#8b5cf6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gradient Color Pickers (shown when toggle is OFF) -->
|
||||||
|
<div class="color-grid-2col" id="trayGradientPickers">
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Normal Start</label>
|
||||||
|
<input type="color" id="trayColorNormalStart" value="#8b5cf6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Normal End</label>
|
||||||
|
<input type="color" id="trayColorNormalEnd" value="#a78bfa">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Warning Start</label>
|
||||||
|
<input type="color" id="trayColorWarningStart" value="#f59e0b">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Warning End</label>
|
||||||
|
<input type="color" id="trayColorWarningEnd" value="#fbbf24">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Danger Start</label>
|
||||||
|
<input type="color" id="trayColorDangerStart" value="#ef4444">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-item">
|
||||||
|
<label>Danger End</label>
|
||||||
|
<input type="color" id="trayColorDangerEnd" value="#f87171">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn-secondary" id="resetTrayColorsBtn">Reset Tray Colors</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Account Actions -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>Account</h2>
|
||||||
|
<button class="btn-danger" id="logoutBtn">Log Out</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Support -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<button class="btn-coffee" id="coffeeBtn">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M17 8h1a4 4 0 1 1 0 8h-1" />
|
||||||
|
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z" />
|
||||||
|
<line x1="6" y1="2" x2="6" y2="4" />
|
||||||
|
<line x1="10" y1="2" x2="10" y2="4" />
|
||||||
|
<line x1="14" y1="2" x2="14" y2="4" />
|
||||||
|
</svg>
|
||||||
|
If you find this useful, buy me a coffee!
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Disclaimer -->
|
||||||
|
<p class="disclaimer">
|
||||||
|
<strong>Disclaimer:</strong> Unofficial tool not affiliated with Anthropic. Use at your own
|
||||||
|
discretion.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="settings.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
483
src/renderer/settings.js
Normal file
483
src/renderer/settings.js
Normal file
|
|
@ -0,0 +1,483 @@
|
||||||
|
// DOM elements
|
||||||
|
const elements = {
|
||||||
|
colorNormalStart: document.getElementById('colorNormalStart'),
|
||||||
|
colorNormalEnd: document.getElementById('colorNormalEnd'),
|
||||||
|
colorWarningStart: document.getElementById('colorWarningStart'),
|
||||||
|
colorWarningEnd: document.getElementById('colorWarningEnd'),
|
||||||
|
colorDangerStart: document.getElementById('colorDangerStart'),
|
||||||
|
colorDangerEnd: document.getElementById('colorDangerEnd'),
|
||||||
|
resetColorsBtn: document.getElementById('resetColorsBtn'),
|
||||||
|
logoutBtn: document.getElementById('logoutBtn'),
|
||||||
|
coffeeBtn: document.getElementById('coffeeBtn'),
|
||||||
|
minimizeBtn: document.getElementById('minimizeBtn'),
|
||||||
|
closeBtn: document.getElementById('closeBtn'),
|
||||||
|
// Tray icon elements
|
||||||
|
trayDisplaySession: document.getElementById('trayDisplaySession'),
|
||||||
|
trayDisplayWeekly: document.getElementById('trayDisplayWeekly'),
|
||||||
|
trayUpdateInterval: document.getElementById('trayUpdateInterval'),
|
||||||
|
trayUpdateValue: document.getElementById('trayUpdateValue'),
|
||||||
|
trayShowText: document.getElementById('trayShowText'),
|
||||||
|
trayShowTextLabel: document.getElementById('trayShowTextLabel'),
|
||||||
|
trayColorNormalStart: document.getElementById('trayColorNormalStart'),
|
||||||
|
trayColorNormalEnd: document.getElementById('trayColorNormalEnd'),
|
||||||
|
trayColorWarningStart: document.getElementById('trayColorWarningStart'),
|
||||||
|
trayColorWarningEnd: document.getElementById('trayColorWarningEnd'),
|
||||||
|
trayColorDangerStart: document.getElementById('trayColorDangerStart'),
|
||||||
|
trayColorDangerEnd: document.getElementById('trayColorDangerEnd'),
|
||||||
|
resetTrayColorsBtn: document.getElementById('resetTrayColorsBtn'),
|
||||||
|
// Static color toggles
|
||||||
|
mainWindowStaticColor: document.getElementById('mainWindowStaticColor'),
|
||||||
|
mainWindowStaticColorPicker: document.getElementById('mainWindowStaticColorPicker'),
|
||||||
|
mainWindowStaticColorValue: document.getElementById('mainWindowStaticColorValue'),
|
||||||
|
mainWindowGradientPickers: document.getElementById('mainWindowGradientPickers'),
|
||||||
|
trayStaticColor: document.getElementById('trayStaticColor'),
|
||||||
|
trayStaticColorPicker: document.getElementById('trayStaticColorPicker'),
|
||||||
|
trayStaticColorValue: document.getElementById('trayStaticColorValue'),
|
||||||
|
trayGradientPickers: document.getElementById('trayGradientPickers'),
|
||||||
|
// Theme elements
|
||||||
|
backgroundColorStart: document.getElementById('backgroundColorStart'),
|
||||||
|
backgroundColorEnd: document.getElementById('backgroundColorEnd'),
|
||||||
|
textColorPrimary: document.getElementById('textColorPrimary'),
|
||||||
|
textColorSecondary: document.getElementById('textColorSecondary'),
|
||||||
|
titleBarBackground: document.getElementById('titleBarBackground'),
|
||||||
|
titleBarOpacity: document.getElementById('titleBarOpacity'),
|
||||||
|
titleBarOpacityValue: document.getElementById('titleBarOpacityValue'),
|
||||||
|
borderColor: document.getElementById('borderColor'),
|
||||||
|
borderOpacity: document.getElementById('borderOpacity'),
|
||||||
|
borderOpacityValue: document.getElementById('borderOpacityValue'),
|
||||||
|
resetThemeBtn: document.getElementById('resetThemeBtn')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
async function init() {
|
||||||
|
setupEventListeners();
|
||||||
|
setupCollapsibleSections();
|
||||||
|
|
||||||
|
// Load and apply color preferences
|
||||||
|
const colorPrefs = await window.electronAPI.getColorPreferences();
|
||||||
|
loadColorPickerValues(colorPrefs);
|
||||||
|
|
||||||
|
// Load and apply tray settings
|
||||||
|
const traySettings = await window.electronAPI.getTraySettings();
|
||||||
|
loadTraySettings(traySettings);
|
||||||
|
|
||||||
|
// Load tray update interval
|
||||||
|
const interval = await window.electronAPI.getTrayUpdateInterval();
|
||||||
|
loadTrayUpdateInterval(interval);
|
||||||
|
|
||||||
|
// Load theme settings
|
||||||
|
await loadThemeSettings();
|
||||||
|
|
||||||
|
// Apply theme to settings window
|
||||||
|
const theme = await window.electronAPI.getThemeSettings();
|
||||||
|
applyThemeToSettings(theme);
|
||||||
|
|
||||||
|
// Listen for theme changes
|
||||||
|
window.electronAPI.onThemeChanged((theme) => {
|
||||||
|
applyThemeToSettings(theme);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
function setupEventListeners() {
|
||||||
|
// Color picker event listeners
|
||||||
|
const colorInputs = [
|
||||||
|
elements.colorNormalStart,
|
||||||
|
elements.colorNormalEnd,
|
||||||
|
elements.colorWarningStart,
|
||||||
|
elements.colorWarningEnd,
|
||||||
|
elements.colorDangerStart,
|
||||||
|
elements.colorDangerEnd
|
||||||
|
];
|
||||||
|
|
||||||
|
colorInputs.forEach(input => {
|
||||||
|
input.addEventListener('change', async () => {
|
||||||
|
await saveCurrentColors();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.resetColorsBtn.addEventListener('click', async () => {
|
||||||
|
const defaults = {
|
||||||
|
normal: { start: '#8b5cf6', end: '#a78bfa' },
|
||||||
|
warning: { start: '#f59e0b', end: '#fbbf24' },
|
||||||
|
danger: { start: '#ef4444', end: '#f87171' }
|
||||||
|
};
|
||||||
|
await window.electronAPI.setColorPreferences(defaults);
|
||||||
|
loadColorPickerValues(defaults);
|
||||||
|
// Notify main window to update colors immediately
|
||||||
|
await window.electronAPI.notifyColorChange(defaults);
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.logoutBtn.addEventListener('click', async () => {
|
||||||
|
await window.electronAPI.deleteCredentials();
|
||||||
|
window.close();
|
||||||
|
window.electronAPI.openLogin();
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.coffeeBtn.addEventListener('click', () => {
|
||||||
|
window.electronAPI.openExternal('https://paypal.me/SlavomirDurej?country.x=GB&locale.x=en_GB');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Window controls
|
||||||
|
elements.minimizeBtn.addEventListener('click', () => {
|
||||||
|
window.electronAPI.minimizeWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.closeBtn.addEventListener('click', () => {
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tray icon event listeners
|
||||||
|
elements.trayDisplaySession.addEventListener('change', saveTraySettings);
|
||||||
|
elements.trayDisplayWeekly.addEventListener('change', saveTraySettings);
|
||||||
|
|
||||||
|
elements.trayShowText.addEventListener('change', () => {
|
||||||
|
elements.trayShowTextLabel.textContent = elements.trayShowText.checked ? 'Enabled' : 'Disabled';
|
||||||
|
saveTraySettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
const trayColorInputs = [
|
||||||
|
elements.trayColorNormalStart,
|
||||||
|
elements.trayColorNormalEnd,
|
||||||
|
elements.trayColorWarningStart,
|
||||||
|
elements.trayColorWarningEnd,
|
||||||
|
elements.trayColorDangerStart,
|
||||||
|
elements.trayColorDangerEnd
|
||||||
|
];
|
||||||
|
|
||||||
|
trayColorInputs.forEach(input => {
|
||||||
|
input.addEventListener('change', saveTraySettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.trayUpdateInterval.addEventListener('input', () => {
|
||||||
|
elements.trayUpdateValue.textContent = elements.trayUpdateInterval.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.trayUpdateInterval.addEventListener('change', () => {
|
||||||
|
window.electronAPI.setTrayUpdateInterval(parseInt(elements.trayUpdateInterval.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.resetTrayColorsBtn.addEventListener('click', async () => {
|
||||||
|
const defaults = {
|
||||||
|
displayMode: elements.trayDisplaySession.checked ? 'session' : 'weekly',
|
||||||
|
showText: false,
|
||||||
|
colors: {
|
||||||
|
normal: { start: '#8b5cf6', end: '#a78bfa' },
|
||||||
|
warning: { start: '#f59e0b', end: '#fbbf24' },
|
||||||
|
danger: { start: '#ef4444', end: '#f87171' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await window.electronAPI.setTraySettings(defaults);
|
||||||
|
loadTraySettings(defaults);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main Window static color toggle
|
||||||
|
elements.mainWindowStaticColor.addEventListener('change', () => {
|
||||||
|
const isStatic = elements.mainWindowStaticColor.checked;
|
||||||
|
elements.mainWindowStaticColorPicker.style.display = isStatic ? 'block' : 'none';
|
||||||
|
elements.mainWindowGradientPickers.style.display = isStatic ? 'none' : 'grid';
|
||||||
|
saveCurrentColors();
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.mainWindowStaticColorValue.addEventListener('change', saveCurrentColors);
|
||||||
|
|
||||||
|
// Tray static color toggle
|
||||||
|
elements.trayStaticColor.addEventListener('change', () => {
|
||||||
|
const isStatic = elements.trayStaticColor.checked;
|
||||||
|
elements.trayStaticColorPicker.style.display = isStatic ? 'block' : 'none';
|
||||||
|
elements.trayGradientPickers.style.display = isStatic ? 'none' : 'grid';
|
||||||
|
saveTraySettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.trayStaticColorValue.addEventListener('change', saveTraySettings);
|
||||||
|
|
||||||
|
// Theme settings
|
||||||
|
const themeInputs = [
|
||||||
|
elements.backgroundColorStart,
|
||||||
|
elements.backgroundColorEnd,
|
||||||
|
elements.textColorPrimary,
|
||||||
|
elements.textColorSecondary,
|
||||||
|
elements.titleBarBackground,
|
||||||
|
elements.borderColor
|
||||||
|
];
|
||||||
|
|
||||||
|
themeInputs.forEach(input => {
|
||||||
|
input.addEventListener('change', saveThemeSettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Opacity sliders
|
||||||
|
elements.titleBarOpacity.addEventListener('input', () => {
|
||||||
|
elements.titleBarOpacityValue.textContent = elements.titleBarOpacity.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.titleBarOpacity.addEventListener('change', saveThemeSettings);
|
||||||
|
|
||||||
|
elements.borderOpacity.addEventListener('input', () => {
|
||||||
|
elements.borderOpacityValue.textContent = elements.borderOpacity.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.borderOpacity.addEventListener('change', saveThemeSettings);
|
||||||
|
|
||||||
|
// Reset theme button
|
||||||
|
elements.resetThemeBtn.addEventListener('click', resetTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function loadColorPickerValues(prefs) {
|
||||||
|
// Check if static mode
|
||||||
|
const isStatic = prefs.isStatic || false;
|
||||||
|
elements.mainWindowStaticColor.checked = isStatic;
|
||||||
|
|
||||||
|
if (isStatic) {
|
||||||
|
// Load static color
|
||||||
|
elements.mainWindowStaticColorValue.value = prefs.staticColor || '#8b5cf6';
|
||||||
|
elements.mainWindowStaticColorPicker.style.display = 'block';
|
||||||
|
elements.mainWindowGradientPickers.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
// Load gradient colors
|
||||||
|
elements.colorNormalStart.value = prefs.normal.start;
|
||||||
|
elements.colorNormalEnd.value = prefs.normal.end;
|
||||||
|
elements.colorWarningStart.value = prefs.warning.start;
|
||||||
|
elements.colorWarningEnd.value = prefs.warning.end;
|
||||||
|
elements.colorDangerStart.value = prefs.danger.start;
|
||||||
|
elements.colorDangerEnd.value = prefs.danger.end;
|
||||||
|
elements.mainWindowStaticColorPicker.style.display = 'none';
|
||||||
|
elements.mainWindowGradientPickers.style.display = 'grid';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCurrentColors() {
|
||||||
|
const isStatic = elements.mainWindowStaticColor.checked;
|
||||||
|
|
||||||
|
let prefs;
|
||||||
|
if (isStatic) {
|
||||||
|
// Use static color for all states
|
||||||
|
const staticColor = elements.mainWindowStaticColorValue.value;
|
||||||
|
prefs = {
|
||||||
|
isStatic: true,
|
||||||
|
staticColor: staticColor,
|
||||||
|
normal: { start: staticColor, end: staticColor },
|
||||||
|
warning: { start: staticColor, end: staticColor },
|
||||||
|
danger: { start: staticColor, end: staticColor }
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Use gradient colors
|
||||||
|
prefs = {
|
||||||
|
isStatic: false,
|
||||||
|
normal: {
|
||||||
|
start: elements.colorNormalStart.value,
|
||||||
|
end: elements.colorNormalEnd.value
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
start: elements.colorWarningStart.value,
|
||||||
|
end: elements.colorWarningEnd.value
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
start: elements.colorDangerStart.value,
|
||||||
|
end: elements.colorDangerEnd.value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.electronAPI.setColorPreferences(prefs);
|
||||||
|
// Notify main window to update colors
|
||||||
|
await window.electronAPI.notifyColorChange(prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tray settings functions
|
||||||
|
function loadTraySettings(settings) {
|
||||||
|
// Set display mode
|
||||||
|
if (settings.displayMode === 'weekly') {
|
||||||
|
elements.trayDisplayWeekly.checked = true;
|
||||||
|
} else {
|
||||||
|
elements.trayDisplaySession.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set show text toggle
|
||||||
|
const showText = settings.showText !== undefined ? settings.showText : false;
|
||||||
|
elements.trayShowText.checked = showText;
|
||||||
|
elements.trayShowTextLabel.textContent = showText ? 'Enabled' : 'Disabled';
|
||||||
|
|
||||||
|
// Check if static mode
|
||||||
|
const isStatic = settings.isStatic || false;
|
||||||
|
elements.trayStaticColor.checked = isStatic;
|
||||||
|
|
||||||
|
if (isStatic) {
|
||||||
|
// Load static color
|
||||||
|
elements.trayStaticColorValue.value = settings.staticColor || '#8b5cf6';
|
||||||
|
elements.trayStaticColorPicker.style.display = 'block';
|
||||||
|
elements.trayGradientPickers.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
// Load gradient colors
|
||||||
|
elements.trayColorNormalStart.value = settings.colors.normal.start;
|
||||||
|
elements.trayColorNormalEnd.value = settings.colors.normal.end;
|
||||||
|
elements.trayColorWarningStart.value = settings.colors.warning.start;
|
||||||
|
elements.trayColorWarningEnd.value = settings.colors.warning.end;
|
||||||
|
elements.trayColorDangerStart.value = settings.colors.danger.start;
|
||||||
|
elements.trayColorDangerEnd.value = settings.colors.danger.end;
|
||||||
|
elements.trayStaticColorPicker.style.display = 'none';
|
||||||
|
elements.trayGradientPickers.style.display = 'grid';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTrayUpdateInterval(interval) {
|
||||||
|
elements.trayUpdateInterval.value = interval;
|
||||||
|
elements.trayUpdateValue.textContent = interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTraySettings() {
|
||||||
|
const isStatic = elements.trayStaticColor.checked;
|
||||||
|
|
||||||
|
let settings;
|
||||||
|
if (isStatic) {
|
||||||
|
// Use static color for all states
|
||||||
|
const staticColor = elements.trayStaticColorValue.value;
|
||||||
|
settings = {
|
||||||
|
displayMode: elements.trayDisplayWeekly.checked ? 'weekly' : 'session',
|
||||||
|
showText: elements.trayShowText.checked,
|
||||||
|
isStatic: true,
|
||||||
|
staticColor: staticColor,
|
||||||
|
colors: {
|
||||||
|
normal: { start: staticColor, end: staticColor },
|
||||||
|
warning: { start: staticColor, end: staticColor },
|
||||||
|
danger: { start: staticColor, end: staticColor }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Use gradient colors
|
||||||
|
settings = {
|
||||||
|
displayMode: elements.trayDisplayWeekly.checked ? 'weekly' : 'session',
|
||||||
|
showText: elements.trayShowText.checked,
|
||||||
|
isStatic: false,
|
||||||
|
colors: {
|
||||||
|
normal: {
|
||||||
|
start: elements.trayColorNormalStart.value,
|
||||||
|
end: elements.trayColorNormalEnd.value
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
start: elements.trayColorWarningStart.value,
|
||||||
|
end: elements.trayColorWarningEnd.value
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
start: elements.trayColorDangerStart.value,
|
||||||
|
end: elements.trayColorDangerEnd.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.electronAPI.setTraySettings(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme settings functions
|
||||||
|
async function loadThemeSettings() {
|
||||||
|
const theme = await window.electronAPI.getThemeSettings();
|
||||||
|
|
||||||
|
elements.backgroundColorStart.value = theme.backgroundStart || '#1e1e2e';
|
||||||
|
elements.backgroundColorEnd.value = theme.backgroundEnd || '#2a2a3e';
|
||||||
|
elements.textColorPrimary.value = theme.textPrimary || '#e0e0e0';
|
||||||
|
elements.textColorSecondary.value = theme.textSecondary || '#a0a0a0';
|
||||||
|
elements.titleBarBackground.value = theme.titleBarBg || '#000000';
|
||||||
|
elements.titleBarOpacity.value = theme.titleBarOpacity || 30;
|
||||||
|
elements.titleBarOpacityValue.textContent = elements.titleBarOpacity.value;
|
||||||
|
elements.borderColor.value = theme.borderColor || '#ffffff';
|
||||||
|
elements.borderOpacity.value = theme.borderOpacity || 10;
|
||||||
|
elements.borderOpacityValue.textContent = elements.borderOpacity.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveThemeSettings() {
|
||||||
|
const theme = {
|
||||||
|
backgroundStart: elements.backgroundColorStart.value,
|
||||||
|
backgroundEnd: elements.backgroundColorEnd.value,
|
||||||
|
textPrimary: elements.textColorPrimary.value,
|
||||||
|
textSecondary: elements.textColorSecondary.value,
|
||||||
|
titleBarBg: elements.titleBarBackground.value,
|
||||||
|
titleBarOpacity: parseInt(elements.titleBarOpacity.value),
|
||||||
|
borderColor: elements.borderColor.value,
|
||||||
|
borderOpacity: parseInt(elements.borderOpacity.value)
|
||||||
|
};
|
||||||
|
|
||||||
|
await window.electronAPI.setThemeSettings(theme);
|
||||||
|
|
||||||
|
// Apply theme to settings window immediately
|
||||||
|
applyThemeToSettings(theme);
|
||||||
|
|
||||||
|
// Notify main window to update
|
||||||
|
await window.electronAPI.notifyThemeChange(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetTheme() {
|
||||||
|
const defaults = {
|
||||||
|
backgroundStart: '#1e1e2e',
|
||||||
|
backgroundEnd: '#2a2a3e',
|
||||||
|
textPrimary: '#e0e0e0',
|
||||||
|
textSecondary: '#a0a0a0',
|
||||||
|
titleBarBg: '#000000',
|
||||||
|
titleBarOpacity: 30,
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
borderOpacity: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
await window.electronAPI.setThemeSettings(defaults);
|
||||||
|
await loadThemeSettings();
|
||||||
|
|
||||||
|
// Apply theme to settings window immediately
|
||||||
|
applyThemeToSettings(defaults);
|
||||||
|
|
||||||
|
// Notify main window to update
|
||||||
|
await window.electronAPI.notifyThemeChange(defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply theme to settings window
|
||||||
|
function applyThemeToSettings(theme) {
|
||||||
|
const settingsApp = document.getElementById('settings-app');
|
||||||
|
const titleBar = document.querySelector('.title-bar');
|
||||||
|
|
||||||
|
// Apply background gradient
|
||||||
|
if (settingsApp) {
|
||||||
|
settingsApp.style.background = `linear-gradient(135deg, ${theme.backgroundStart} 0%, ${theme.backgroundEnd} 100%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply title bar background
|
||||||
|
if (titleBar) {
|
||||||
|
const opacity = theme.titleBarOpacity / 100;
|
||||||
|
const bgColor = theme.titleBarBg;
|
||||||
|
titleBar.style.background = `rgba(${parseInt(bgColor.slice(1, 3), 16)}, ${parseInt(bgColor.slice(3, 5), 16)}, ${parseInt(bgColor.slice(5, 7), 16)}, ${opacity})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply text colors via CSS variables
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.style.setProperty('--text-primary', theme.textPrimary);
|
||||||
|
root.style.setProperty('--text-secondary', theme.textSecondary);
|
||||||
|
|
||||||
|
// Update primary text color (titles, labels)
|
||||||
|
const primaryTextElements = document.querySelectorAll('.title span, h2, h3, .radio-label, .toggle-label, .setting-description');
|
||||||
|
primaryTextElements.forEach(el => {
|
||||||
|
if (el.classList.contains('setting-description')) {
|
||||||
|
// Secondary text for descriptions
|
||||||
|
el.style.color = theme.textSecondary;
|
||||||
|
} else {
|
||||||
|
// Primary text for titles/labels
|
||||||
|
el.style.color = theme.textPrimary;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup collapsible sections
|
||||||
|
function setupCollapsibleSections() {
|
||||||
|
const collapsibleHeaders = document.querySelectorAll('.collapsible-header');
|
||||||
|
|
||||||
|
collapsibleHeaders.forEach(header => {
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const content = header.nextElementSibling;
|
||||||
|
const isActive = header.classList.contains('active');
|
||||||
|
|
||||||
|
// Toggle active state
|
||||||
|
header.classList.toggle('active');
|
||||||
|
content.classList.toggle('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the application
|
||||||
|
init();
|
||||||
|
|
@ -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;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -36,6 +50,7 @@ body {
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
|
padding-left: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -49,6 +64,16 @@ body {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-logo {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
border-radius: 6px;
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
|
|
@ -177,6 +202,90 @@ body {
|
||||||
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
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 */
|
/* Main Content */
|
||||||
.content {
|
.content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
@ -244,7 +353,7 @@ body {
|
||||||
|
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
height: 100%;
|
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;
|
border-radius: 3px;
|
||||||
transition: width 0.6s ease;
|
transition: width 0.6s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -254,8 +363,8 @@ body {
|
||||||
/* Shimmer removed */
|
/* Shimmer removed */
|
||||||
|
|
||||||
.progress-fill.weekly {
|
.progress-fill.weekly {
|
||||||
background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%);
|
background: linear-gradient(90deg, var(--color-normal-start) 0%, var(--color-normal-end) 100%);
|
||||||
box-shadow: 0 0 10px rgba(59, 130, 246, 0.3);
|
box-shadow: 0 0 10px rgba(139, 92, 246, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -283,13 +392,13 @@ body {
|
||||||
|
|
||||||
.timer-progress {
|
.timer-progress {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: #8b5cf6;
|
stroke: var(--color-normal-start);
|
||||||
stroke-width: 4;
|
stroke-width: 4;
|
||||||
transition: stroke-dashoffset 0.6s ease;
|
transition: stroke-dashoffset 0.6s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timer-progress.weekly {
|
.timer-progress.weekly {
|
||||||
stroke: #3b82f6;
|
stroke: var(--color-normal-start);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timer-text {
|
.timer-text {
|
||||||
|
|
@ -450,31 +559,31 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Warning states */
|
/* Warning states */
|
||||||
.progress-fill.warning {
|
.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 {
|
.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 {
|
.timer-progress.warning {
|
||||||
stroke: #f59e0b;
|
stroke: var(--color-warning-start);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timer-progress.danger {
|
.timer-progress.danger {
|
||||||
stroke: #ef4444;
|
stroke: var(--color-danger-start);
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue