Compare commits
10 commits
9a6fd5dd18
...
5f9ffff8f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f9ffff8f6 | ||
| d616b885b5 | |||
|
|
cb33ed9619 | ||
|
|
568503f3e0 | ||
|
|
8500ccfaad | ||
|
|
ba504455e8 | ||
|
|
8c21bcba74 | ||
|
|
7dab135369 | ||
|
|
b1fb98539f | ||
|
|
f72cbf9e91 |
21 changed files with 2938 additions and 1 deletions
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dir:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Electron
|
||||
*.electron/
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
.temp/
|
||||
CLAUDE_NOTES.md
|
||||
39
CHANGES.md
Normal file
39
CHANGES.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Changes for Upstream PR
|
||||
|
||||
## Feature: Customizable Progress Bar Colors
|
||||
|
||||
### Modified Files
|
||||
|
||||
#### main.js
|
||||
- Added `DEFAULT_COLOR_PREFERENCES` constant with default color scheme (normal, warning, danger states)
|
||||
- Added IPC handler `get-color-preferences` to retrieve stored color preferences from electron-store
|
||||
- Added IPC handler `set-color-preferences` to save color preferences to electron-store
|
||||
- Storage schema: `colorPreferences: { normal: {start, end}, warning: {start, end}, danger: {start, end} }`
|
||||
|
||||
#### src/renderer/styles.css
|
||||
- TODO: Add CSS custom properties (--color-normal-start, --color-normal-end, etc.)
|
||||
- TODO: Convert hardcoded gradient colors to use CSS variables
|
||||
- TODO: Apply to both .progress-fill and .timer-progress elements
|
||||
|
||||
#### src/renderer/app.js
|
||||
- TODO: Add `applyColorPreferences()` function to set CSS custom properties
|
||||
- TODO: Load and apply color preferences on init
|
||||
- TODO: Add event listeners for color picker changes
|
||||
|
||||
#### src/renderer/index.html
|
||||
- TODO: Add color picker UI in settings overlay (6 inputs for simplified scheme)
|
||||
- TODO: Add "Reset to Defaults" button
|
||||
|
||||
#### preload.js
|
||||
- TODO: Expose `getColorPreferences` and `setColorPreferences` IPC methods
|
||||
|
||||
---
|
||||
|
||||
## Feature: Dynamic Tray Icon with Usage Display (Planned)
|
||||
- Not yet started
|
||||
|
||||
---
|
||||
|
||||
## Storage Changes
|
||||
- Using existing electron-store, no additional files
|
||||
- New keys: `colorPreferences`, `trayDisplayMode` (planned)
|
||||
89
INSTALL.md
Normal file
89
INSTALL.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# Installation Instructions
|
||||
|
||||
## For End Users
|
||||
|
||||
### Option 1: Download Installer (Recommended)
|
||||
1. Download `Claude-Usage-Widget-Setup.exe` from releases
|
||||
2. Run the installer
|
||||
3. Launch from Start Menu
|
||||
4. Login when prompted
|
||||
|
||||
### Option 2: Build from Source
|
||||
```bash
|
||||
# Install Node.js from https://nodejs.org (if not already installed)
|
||||
|
||||
# Clone or download this project
|
||||
cd claude-usage-widget
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run the widget
|
||||
npm start
|
||||
|
||||
# Or build installer
|
||||
npm run build:win
|
||||
```
|
||||
|
||||
## First Time Setup
|
||||
|
||||
1. **Launch the widget** - A frameless window appears
|
||||
2. **Click "Login to Claude"** - Browser window opens
|
||||
3. **Login to Claude.ai** - Use your normal credentials
|
||||
4. **Widget activates** - Usage data appears automatically
|
||||
5. **Minimize to tray** - Click the minus icon
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **OS:** Windows 10 or later (64-bit)
|
||||
- **RAM:** 200 MB
|
||||
- **Disk:** 100 MB
|
||||
- **Internet:** Required for Claude.ai API
|
||||
|
||||
## What Gets Installed
|
||||
|
||||
- Executable: `%LOCALAPPDATA%\Programs\claude-usage-widget\`
|
||||
- Settings: `%APPDATA%\claude-usage-widget\` (encrypted)
|
||||
- Start Menu shortcut
|
||||
- Desktop shortcut (optional)
|
||||
|
||||
## Uninstallation
|
||||
|
||||
**Windows:**
|
||||
1. Settings → Apps → Claude Usage Widget → Uninstall
|
||||
2. Or run `Uninstall Claude Usage Widget.exe` from install directory
|
||||
|
||||
**Manual cleanup:**
|
||||
```
|
||||
%APPDATA%\claude-usage-widget\
|
||||
%LOCALAPPDATA%\Programs\claude-usage-widget\
|
||||
```
|
||||
|
||||
## Troubleshooting Install Issues
|
||||
|
||||
### "Windows protected your PC"
|
||||
1. Click "More info"
|
||||
2. Click "Run anyway"
|
||||
3. This is normal for unsigned apps
|
||||
|
||||
### Installer won't run
|
||||
- Ensure you have admin rights
|
||||
- Disable antivirus temporarily
|
||||
- Download again (file may be corrupted)
|
||||
|
||||
### Can't find after install
|
||||
- Check Start Menu → All Apps
|
||||
- Search for "Claude Usage Widget"
|
||||
- Check Desktop for shortcut
|
||||
|
||||
## Security Notes
|
||||
|
||||
✅ **Your data stays local** - credentials stored encrypted on your machine
|
||||
✅ **Open source** - code is available for review
|
||||
✅ **No telemetry** - no usage data sent anywhere
|
||||
✅ **Direct API** - only communicates with claude.ai
|
||||
|
||||
---
|
||||
|
||||
For development setup, see [QUICKSTART.md](QUICKSTART.md)
|
||||
For full documentation, see [README.md](README.md)
|
||||
195
QUICKSTART.md
Normal file
195
QUICKSTART.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# Quick Start Guide
|
||||
|
||||
## Installation & Development
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
cd claude-usage-widget
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Run in Development Mode
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
This will:
|
||||
- Launch the widget with DevTools open
|
||||
- Enable hot-reload for debugging
|
||||
- Show console logs
|
||||
|
||||
### 3. Test the Application
|
||||
|
||||
**First Run:**
|
||||
1. Widget appears (frameless window)
|
||||
2. Click "Login to Claude"
|
||||
3. Browser window opens to claude.ai
|
||||
4. Login with your credentials
|
||||
5. Widget automatically captures session
|
||||
6. Usage data displays
|
||||
|
||||
**Features to Test:**
|
||||
- [ ] Drag widget around screen
|
||||
- [ ] Refresh button updates data
|
||||
- [ ] Minimize to system tray
|
||||
- [ ] Right-click tray icon shows menu
|
||||
- [ ] Progress bars animate smoothly
|
||||
- [ ] Timers count down correctly
|
||||
- [ ] Re-login from tray menu works
|
||||
|
||||
### 4. Build for Production
|
||||
|
||||
```bash
|
||||
npm run build:win
|
||||
```
|
||||
|
||||
Output: `dist/Claude-Usage-Widget-Setup.exe`
|
||||
|
||||
## Development Tips
|
||||
|
||||
### Enable DevTools
|
||||
Already enabled in dev mode. To disable, edit `main.js`:
|
||||
```javascript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Comment out this line:
|
||||
// mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||
}
|
||||
```
|
||||
|
||||
### Test Without Building
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
### Debug Authentication
|
||||
Check the console for:
|
||||
- Cookie capture events
|
||||
- Organization ID extraction
|
||||
- API responses
|
||||
|
||||
### Change Update Frequency
|
||||
Edit `src/renderer/app.js`:
|
||||
```javascript
|
||||
const UPDATE_INTERVAL = 1 * 60 * 1000; // 1 minute for testing
|
||||
```
|
||||
|
||||
### Mock API Response
|
||||
For testing UI without API calls, add to `fetchUsageData()`:
|
||||
```javascript
|
||||
const mockData = {
|
||||
five_hour: { utilization: 45.5, resets_at: "2025-12-13T20:00:00Z" },
|
||||
seven_day: { utilization: 78.2, resets_at: "2025-12-17T07:00:00Z" }
|
||||
};
|
||||
updateUI(mockData);
|
||||
return;
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
claude-usage-widget/
|
||||
├── main.js # Electron main process
|
||||
├── preload.js # IPC bridge
|
||||
├── package.json # Dependencies & build config
|
||||
├── src/
|
||||
│ └── renderer/
|
||||
│ ├── index.html # Widget UI
|
||||
│ ├── styles.css # Styling
|
||||
│ └── app.js # Frontend logic
|
||||
└── assets/
|
||||
├── icon.ico # App icon
|
||||
└── tray-icon.png # Tray icon
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Port Already in Use
|
||||
Electron doesn't use ports, so this shouldn't happen.
|
||||
|
||||
### White Screen on Launch
|
||||
Check console for errors. Usually means:
|
||||
- Missing file paths
|
||||
- JavaScript errors in app.js
|
||||
- CSS not loading
|
||||
|
||||
### Login Window Not Capturing Session
|
||||
Check `main.js` - the `did-finish-load` event handler should:
|
||||
1. Check URL contains 'chat' or 'new'
|
||||
2. Extract sessionKey cookie
|
||||
3. Try to get organization ID
|
||||
|
||||
### API Returns 401
|
||||
Session expired. Click "Re-login" from tray menu.
|
||||
|
||||
## Adding Features
|
||||
|
||||
### Custom Themes
|
||||
Edit `styles.css` - change gradient colors:
|
||||
```css
|
||||
.widget-container {
|
||||
background: linear-gradient(135deg, #your-color 0%, #another-color 100%);
|
||||
}
|
||||
```
|
||||
|
||||
### Notification Alerts
|
||||
Add to `updateUI()` in `app.js`:
|
||||
```javascript
|
||||
if (weeklyUtilization >= 90) {
|
||||
new Notification('Claude Usage Alert', {
|
||||
body: 'You\'re at 90% of weekly limit!'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Keyboard Shortcuts
|
||||
Add to `main.js`:
|
||||
```javascript
|
||||
const { globalShortcut } = require('electron');
|
||||
|
||||
app.whenReady().then(() => {
|
||||
globalShortcut.register('CommandOrControl+Shift+C', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Console Logs
|
||||
- Main process: Check terminal where you ran `npm start`
|
||||
- Renderer process: Check DevTools console (F12)
|
||||
|
||||
### Network Requests
|
||||
DevTools → Network tab shows all API calls
|
||||
|
||||
### Storage
|
||||
Check stored credentials:
|
||||
```javascript
|
||||
// In DevTools console:
|
||||
await window.electronAPI.getCredentials()
|
||||
```
|
||||
|
||||
## Publishing
|
||||
|
||||
1. Update version in `package.json`
|
||||
2. Run `npm run build:win`
|
||||
3. Test the installer in `dist/`
|
||||
4. Create GitHub release
|
||||
5. Upload the `.exe` file
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [ ] Add app icon (`.ico` file)
|
||||
- [ ] Add tray icon (16x16 PNG)
|
||||
- [ ] Test on clean Windows machine
|
||||
- [ ] Create installer screenshots
|
||||
- [ ] Write changelog
|
||||
- [ ] Submit to releases
|
||||
|
||||
---
|
||||
|
||||
Happy coding! 🚀
|
||||
188
README.md
188
README.md
|
|
@ -1,2 +1,188 @@
|
|||
# claude-usage-widget
|
||||
# Claude Usage Widget
|
||||
|
||||
A beautiful, standalone Windows desktop widget that displays your Claude.ai usage statistics in real-time.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- 🎯 **Real-time Usage Tracking** - Monitor both session and weekly usage limits
|
||||
- 📊 **Visual Progress Bars** - Clean, gradient progress indicators
|
||||
- ⏱️ **Countdown Timers** - Circular timers showing time until reset
|
||||
- 🔄 **Auto-refresh** - Updates every 5 minutes automatically
|
||||
- 🎨 **Modern UI** - Sleek, draggable widget with dark theme
|
||||
- 🔒 **Secure** - Encrypted credential storage
|
||||
- 📍 **Always on Top** - Stays visible across all workspaces
|
||||
- 💾 **System Tray** - Minimizes to tray for easy access
|
||||
|
||||
## Installation
|
||||
|
||||
### Download Pre-built Release
|
||||
1. Download the latest `Claude-Usage-Widget-Setup.exe` from [Releases](https://github.com/SlavomirDurej/claude-usage-widget/releases)
|
||||
2. Run the installer
|
||||
3. Launch "Claude Usage Widget" from Start Menu
|
||||
|
||||
### Build from Source
|
||||
|
||||
**Prerequisites:**
|
||||
- Node.js 18+ ([Download](https://nodejs.org))
|
||||
- npm (comes with Node.js)
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/yourusername/claude-usage-widget.git
|
||||
cd claude-usage-widget
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run in development mode
|
||||
npm start
|
||||
|
||||
# Build installer for Windows
|
||||
npm run build:win
|
||||
```
|
||||
|
||||
The installer will be created in the `dist/` folder.
|
||||
|
||||
## Usage
|
||||
|
||||
### First Launch
|
||||
|
||||
1. Launch the widget
|
||||
2. Click "Login to Claude" when prompted
|
||||
3. A browser window will open - login to your Claude.ai account
|
||||
4. The widget will automatically capture your session
|
||||
5. Usage data will start displaying immediately
|
||||
|
||||
### Widget Controls
|
||||
|
||||
- **Drag** - Click and drag the title bar to move the widget
|
||||
- **Refresh** - Click the refresh icon to update data immediately
|
||||
- **Minimize** - Click the minus icon to hide to system tray
|
||||
- **Close** - Click the X to minimize to tray (doesn't exit)
|
||||
|
||||
### System Tray Menu
|
||||
|
||||
Right-click the tray icon for:
|
||||
- Show/Hide widget
|
||||
- Refresh usage data
|
||||
- Re-login (if session expires)
|
||||
- Settings (coming soon)
|
||||
- Exit application
|
||||
|
||||
## Understanding the Display
|
||||
|
||||
### Current Session
|
||||
- **Progress Bar** - Shows usage from 0-100%
|
||||
- **Timer** - Time remaining until 5-hour session resets
|
||||
- **Color Coding**:
|
||||
- Purple: Normal usage (0-74%)
|
||||
- Orange: High usage (75-89%)
|
||||
- Red: Critical usage (90-100%)
|
||||
|
||||
### Weekly Limit
|
||||
- **Progress Bar** - Shows weekly usage from 0-100%
|
||||
- **Timer** - Time remaining until weekly reset (Wednesdays 7:00 AM)
|
||||
- **Same color coding** as session usage
|
||||
|
||||
## Configuration
|
||||
|
||||
### Auto-start on Windows Boot
|
||||
|
||||
1. Press `Win + R`
|
||||
2. Type `shell:startup` and press Enter
|
||||
3. Create a shortcut to the widget executable in this folder
|
||||
|
||||
### Custom Refresh Interval
|
||||
|
||||
Edit `src/renderer/app.js`:
|
||||
```javascript
|
||||
const UPDATE_INTERVAL = 5 * 60 * 1000; // Change to your preference (in milliseconds)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Login Required" keeps appearing
|
||||
- Your Claude.ai session may have expired
|
||||
- Click "Login to Claude" to re-authenticate
|
||||
- Check that you're logging into the correct account
|
||||
|
||||
### Widget not updating
|
||||
- Check your internet connection
|
||||
- Click the refresh button manually
|
||||
- Ensure Claude.ai is accessible in your region
|
||||
- Try re-logging in from the system tray menu
|
||||
|
||||
### Widget position not saving
|
||||
- Window position is now saved automatically when you drag it
|
||||
- Position will be restored when you restart the app
|
||||
|
||||
### Build errors
|
||||
```bash
|
||||
# Clear cache and reinstall
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
## Privacy & Security
|
||||
|
||||
- Your session credentials are stored **locally only** using encrypted storage
|
||||
- No data is sent to any third-party servers
|
||||
- The widget only communicates with Claude.ai official API
|
||||
- Session cookies are stored using Electron's secure storage
|
||||
|
||||
## Technical Details
|
||||
|
||||
**Built with:**
|
||||
- Electron 28.0.0
|
||||
- Pure JavaScript (no framework overhead)
|
||||
- Native Node.js APIs
|
||||
- electron-store for secure storage
|
||||
|
||||
**API Endpoint:**
|
||||
```
|
||||
https://claude.ai/api/organizations/{org_id}/usage
|
||||
```
|
||||
|
||||
**Storage Location:**
|
||||
```
|
||||
%APPDATA%/claude-usage-widget/config.json (encrypted)
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] macOS support
|
||||
- [ ] Linux support
|
||||
- [ ] Custom themes
|
||||
- [ ] Notification alerts at usage thresholds
|
||||
- [x] Remember window position
|
||||
- [ ] Settings panel
|
||||
- [ ] Usage history graphs
|
||||
- [ ] Multiple account support
|
||||
- [ ] Keyboard shortcuts
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - feel free to use and modify as needed.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This is an unofficial tool and is not affiliated with or endorsed by Anthropic. Use at your own discretion.
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues:
|
||||
1. Check the [Issues](issues) page
|
||||
2. Create a new issue with details about your problem
|
||||
3. Include your OS version and any error messages
|
||||
|
||||
---
|
||||
|
||||
Made with ❤️ for the Claude.ai community
|
||||
|
|
|
|||
75
assets/ICONS-NEEDED.md
Normal file
75
assets/ICONS-NEEDED.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Icon Assets Required
|
||||
|
||||
## What You Need
|
||||
|
||||
### 1. App Icon (icon.ico)
|
||||
**Location:** `assets/icon.ico`
|
||||
**Requirements:**
|
||||
- Format: .ico file
|
||||
- Sizes: 16x16, 32x32, 48x48, 256x256 (multi-resolution)
|
||||
- Design: Claude-themed logo or "C" symbol
|
||||
- Style: Should match the purple/blue gradient theme
|
||||
|
||||
**Quick Creation:**
|
||||
- Use an online tool like https://favicon.io or https://convertio.co
|
||||
- Upload a PNG (256x256) of your design
|
||||
- Convert to .ico with multiple sizes
|
||||
|
||||
### 2. Tray Icon (tray-icon.png)
|
||||
**Location:** `assets/tray-icon.png`
|
||||
**Requirements:**
|
||||
- Format: PNG with transparency
|
||||
- Size: 16x16 or 32x32 pixels
|
||||
- Design: Simple, recognizable at small size
|
||||
- Style: Monochrome or minimal color (for visibility on light/dark taskbars)
|
||||
|
||||
**Tips:**
|
||||
- Keep it simple - just a "C" or claude symbol
|
||||
- Use white/light color for dark taskbars
|
||||
- Test on both dark and light Windows themes
|
||||
|
||||
## Design Suggestions
|
||||
|
||||
### Color Palette (from widget)
|
||||
- Primary Purple: #8b5cf6
|
||||
- Light Purple: #a78bfa
|
||||
- Blue: #3b82f6
|
||||
- Background: #1e1e2e
|
||||
|
||||
### Icon Ideas
|
||||
1. **Letter "C"** in gradient circle
|
||||
2. **Chat bubble** with "C" inside
|
||||
3. **Brain/AI symbol** in purple
|
||||
4. **Minimalist robot** head
|
||||
5. **Abstract neural network** pattern
|
||||
|
||||
## Temporary Solution
|
||||
|
||||
Until you create custom icons, you can use:
|
||||
- Placeholder icon.ico from any Electron template
|
||||
- Or remove icon references from `main.js` temporarily
|
||||
|
||||
```javascript
|
||||
// In main.js, comment out:
|
||||
// tray = new Tray(path.join(__dirname, 'assets/tray-icon.png'));
|
||||
```
|
||||
|
||||
## Online Tools
|
||||
|
||||
- **Icon Generator:** https://www.icongenerator.com
|
||||
- **ICO Converter:** https://convertio.co/png-ico/
|
||||
- **Free Icons:** https://icons8.com or https://www.flaticon.com
|
||||
|
||||
## Example Commands (if you have ImageMagick)
|
||||
|
||||
```bash
|
||||
# Create .ico from PNG
|
||||
convert icon-256.png -define icon:auto-resize=256,128,96,64,48,32,16 icon.ico
|
||||
|
||||
# Create tray icon
|
||||
convert logo.png -resize 16x16 tray-icon.png
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Note:** The app will still work without icons, but they improve the user experience significantly!
|
||||
20
assets/claude-logo.svg
Normal file
20
assets/claude-logo.svg
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="katman_1" data-name="katman 1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 841.89 595.28">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #0f0f0d;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #d97757;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-2" d="M105.01,322.07l29.14-16.35.49-1.42-.49-.79h-1.42l-4.87-.3-16.65-.45-14.44-.6-13.99-.75-3.52-.75-3.3-4.35.34-2.17,2.96-1.99,4.24.37,9.37.64,14.06.97,10.2.6,15.11,1.57h2.4l.34-.97-.82-.6-.64-.6-14.55-9.86-15.75-10.42-8.25-6-4.46-3.04-2.25-2.85-.97-6.22,4.05-4.46,5.44.37,1.39.37,5.51,4.24,11.77,9.11,15.37,11.32,2.25,1.87.9-.64.11-.45-1.01-1.69-8.36-15.11-8.92-15.37-3.97-6.37-1.05-3.82c-.37-1.57-.64-2.89-.64-4.5l4.61-6.26,2.55-.82,6.15.82,2.59,2.25,3.82,8.74,6.19,13.76,9.6,18.71,2.81,5.55,1.5,5.14.56,1.57h.97v-.9l.79-10.54,1.46-12.94,1.42-16.65.49-4.69,2.32-5.62,4.61-3.04,3.6,1.72,2.96,4.24-.41,2.74-1.76,11.44-3.45,17.92-2.25,12h1.31l1.5-1.5,6.07-8.06,10.2-12.75,4.5-5.06,5.25-5.59,3.37-2.66h6.37l4.69,6.97-2.1,7.2-6.56,8.32-5.44,7.05-7.8,10.5-4.87,8.4.45.67,1.16-.11,17.62-3.75,9.52-1.72,11.36-1.95,5.14,2.4.56,2.44-2.02,4.99-12.15,3-14.25,2.85-21.22,5.02-.26.19.3.37,9.56.9,4.09.22h10.01l18.64,1.39,4.87,3.22,2.92,3.94-.49,3-7.5,3.82-10.12-2.4-23.62-5.62-8.1-2.02h-1.12v.67l6.75,6.6,12.37,11.17,15.49,14.4.79,3.56-1.99,2.81-2.1-.3-13.61-10.24-5.25-4.61-11.89-10.01h-.79v1.05l2.74,4.01,14.47,21.75.75,6.67-1.05,2.17-3.75,1.31-4.12-.75-8.47-11.89-8.74-13.39-7.05-12-.86.49-4.16,44.81-1.95,2.29-4.5,1.72-3.75-2.85-1.99-4.61,1.99-9.11,2.4-11.89,1.95-9.45,1.76-11.74,1.05-3.9-.07-.26-.86.11-8.85,12.15-13.46,18.19-10.65,11.4-2.55,1.01-4.42-2.29.41-4.09,2.47-3.64,14.74-18.75,8.89-11.62,5.74-6.71-.04-.97h-.34l-39.15,25.42-6.97.9-3-2.81.37-4.61,1.42-1.5,11.77-8.1-.04.04Z" shape-rendering="optimizeQuality"/>
|
||||
<path class="cls-1" d="M317.73,349.33c-18.82,0-31.69-10.5-37.76-26.66-3.17-8.42-4.74-17.36-4.61-26.36,0-27.11,12.15-45.94,39-45.94,18.04,0,29.17,7.87,35.51,26.66h7.72l-1.05-25.91c-10.8-6.97-24.3-10.5-40.72-10.5-23.14,0-42.82,10.35-53.77,29.02-5.66,9.86-8.53,21.07-8.32,32.44,0,20.74,9.79,39.11,28.16,49.31,10.06,5.37,21.34,8.04,32.74,7.72,17.92,0,32.14-3.41,44.74-9.37l3.26-28.57h-7.87c-4.72,13.05-10.35,20.89-19.69,25.05-4.57,2.06-10.35,3.11-17.32,3.11ZM398.91,250.37l.75-12.75h-5.32l-23.7,7.12v3.86l10.5,4.87v89.17c0,6.07-3.11,7.42-11.25,8.44v6.52h40.31v-6.52c-8.17-1.01-11.25-2.36-11.25-8.44v-92.24l-.04-.04ZM559.22,359.12h3.11l27.26-5.17v-6.67l-3.82-.3c-6.37-.6-8.02-1.91-8.02-7.12v-47.55l.75-15.26h-4.31l-25.76,3.71v6.52l2.51.45c6.97,1.01,9.04,2.96,9.04,7.84v42.37c-6.67,5.17-13.05,8.44-20.62,8.44-8.4,0-13.61-4.27-13.61-14.25v-39.79l.75-15.26h-4.42l-25.8,3.71v6.52l2.66.45c6.97,1.01,9.04,2.96,9.04,7.84v39.11c0,16.57,9.37,24.45,24.3,24.45,11.4,0,20.74-6.07,27.75-14.51l-.75,14.51-.04-.04ZM484.3,306.36c0-21.19-11.25-29.32-31.57-29.32-17.92,0-30.94,7.42-30.94,19.72,0,3.67,1.31,6.49,3.97,8.44l13.65-1.8c-.6-4.12-.9-6.64-.9-7.69,0-6.97,3.71-10.5,11.25-10.5,11.14,0,16.76,7.84,16.76,20.44v4.12l-28.12,8.44c-9.37,2.55-14.7,4.76-18.26,9.94-1.89,3.17-2.8,6.82-2.62,10.5,0,12,8.25,20.47,22.35,20.47,10.2,0,19.24-4.61,27.11-13.35,2.81,8.74,7.12,13.35,14.81,13.35,6.22,0,11.85-2.51,16.87-7.42l-1.5-5.17c-2.17.6-4.27.9-6.49.9-4.31,0-6.37-3.41-6.37-10.09v-30.97ZM448.3,347.12c-7.69,0-12.45-4.46-12.45-12.3,0-5.32,2.51-8.44,7.87-10.24l22.8-7.24v21.9c-7.27,5.51-11.55,7.87-18.22,7.87ZM685.66,353.94v-6.67l-3.86-.3c-6.37-.6-7.99-1.91-7.99-7.12v-89.47l.75-12.75h-5.36l-23.7,7.12v3.86l10.5,4.87v29.32c-5.91-4.05-12.98-6.08-20.14-5.77-23.55,0-41.92,17.92-41.92,44.74,0,22.09,13.2,37.35,34.95,37.35,11.25,0,21.04-5.47,27.11-13.95l-.75,13.95h3.15l27.26-5.17h0ZM636.31,285.92c11.25,0,19.69,6.52,19.69,18.52v33.75c-5.18,5.16-12.23,8-19.54,7.87-16.12,0-24.3-12.75-24.3-29.77,0-19.12,9.34-30.37,24.15-30.37ZM743.3,302.8c-2.1-9.9-8.17-15.52-16.61-15.52-12.6,0-21.34,9.49-21.34,23.1,0,20.14,10.65,33.19,27.86,33.19,11.48-.12,22.04-6.33,27.71-16.31l5.02,1.35c-2.25,17.47-18.07,30.52-37.5,30.52-22.8,0-38.51-16.87-38.51-40.87s17.06-41.21,39.86-41.21c17.02,0,29.02,10.24,32.89,28.01l-59.4,18.22v-8.02l40.01-12.41v-.04Z" shape-rendering="optimizeQuality"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
BIN
assets/claude-usage-screenshot.jpg
Normal file
BIN
assets/claude-usage-screenshot.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/icon.ico
Normal file
BIN
assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
BIN
assets/tray-icon.png
Normal file
BIN
assets/tray-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 478 KiB |
600
main.js
Normal file
600
main.js
Normal file
|
|
@ -0,0 +1,600 @@
|
|||
const { app, BrowserWindow, ipcMain, Tray, Menu, session, shell } = require('electron');
|
||||
const path = require('path');
|
||||
const Store = require('electron-store');
|
||||
const axios = require('axios');
|
||||
|
||||
const store = new Store({
|
||||
encryptionKey: 'claude-widget-secure-key-2024'
|
||||
});
|
||||
|
||||
let mainWindow = null;
|
||||
let loginWindow = null;
|
||||
let silentLoginWindow = null;
|
||||
let settingsWindow = null;
|
||||
let tray = null;
|
||||
|
||||
// Window configuration
|
||||
const WIDGET_WIDTH = 480;
|
||||
const WIDGET_HEIGHT = 140;
|
||||
const SETTINGS_WIDTH = 500;
|
||||
const SETTINGS_HEIGHT = 680;
|
||||
|
||||
function createMainWindow() {
|
||||
// Load saved position or use defaults
|
||||
const savedPosition = store.get('windowPosition');
|
||||
const windowOptions = {
|
||||
width: WIDGET_WIDTH,
|
||||
height: WIDGET_HEIGHT,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
alwaysOnTop: true,
|
||||
resizable: false,
|
||||
skipTaskbar: false,
|
||||
icon: path.join(__dirname, 'assets/icon.ico'),
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
};
|
||||
|
||||
// Apply saved position if it exists
|
||||
if (savedPosition) {
|
||||
windowOptions.x = savedPosition.x;
|
||||
windowOptions.y = savedPosition.y;
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow(windowOptions);
|
||||
|
||||
mainWindow.loadFile('src/renderer/index.html');
|
||||
|
||||
// Make window draggable
|
||||
mainWindow.setAlwaysOnTop(true, 'floating');
|
||||
mainWindow.setVisibleOnAllWorkspaces(true);
|
||||
|
||||
// Save position when window is moved
|
||||
mainWindow.on('move', () => {
|
||||
const position = mainWindow.getBounds();
|
||||
store.set('windowPosition', { x: position.x, y: position.y });
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
// Development tools
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||
}
|
||||
}
|
||||
|
||||
function createLoginWindow() {
|
||||
loginWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 700,
|
||||
parent: mainWindow,
|
||||
modal: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
}
|
||||
});
|
||||
|
||||
loginWindow.loadURL('https://claude.ai');
|
||||
|
||||
let loginCheckInterval = null;
|
||||
let hasLoggedIn = false;
|
||||
|
||||
// Function to check login status
|
||||
async function checkLoginStatus() {
|
||||
if (hasLoggedIn || !loginWindow) 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('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('Org ID fetched from API:', orgId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('API not ready yet:', err.message);
|
||||
}
|
||||
|
||||
if (sessionKey && orgId) {
|
||||
hasLoggedIn = true;
|
||||
if (loginCheckInterval) {
|
||||
clearInterval(loginCheckInterval);
|
||||
loginCheckInterval = null;
|
||||
}
|
||||
|
||||
console.log('Sending login-success to main window...');
|
||||
store.set('sessionKey', sessionKey);
|
||||
store.set('organizationId', orgId);
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('login-success', { sessionKey, organizationId: orgId });
|
||||
console.log('login-success sent');
|
||||
} else {
|
||||
console.error('mainWindow is null, cannot send login-success');
|
||||
}
|
||||
|
||||
loginWindow.close();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in login check:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Check on page load
|
||||
loginWindow.webContents.on('did-finish-load', async () => {
|
||||
const url = loginWindow.webContents.getURL();
|
||||
console.log('Login page loaded:', url);
|
||||
|
||||
if (url.includes('claude.ai')) {
|
||||
await checkLoginStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// Also check on navigation (URL changes)
|
||||
loginWindow.webContents.on('did-navigate', async (event, url) => {
|
||||
console.log('Navigated to:', url);
|
||||
if (url.includes('claude.ai')) {
|
||||
await checkLoginStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// Poll periodically in case the session becomes ready without a page navigation
|
||||
loginCheckInterval = setInterval(async () => {
|
||||
if (!hasLoggedIn && loginWindow) {
|
||||
await checkLoginStatus();
|
||||
} else if (loginCheckInterval) {
|
||||
clearInterval(loginCheckInterval);
|
||||
loginCheckInterval = null;
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
loginWindow.on('closed', () => {
|
||||
if (loginCheckInterval) {
|
||||
clearInterval(loginCheckInterval);
|
||||
loginCheckInterval = null;
|
||||
}
|
||||
loginWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
// Attempt silent login in a hidden browser window
|
||||
async function attemptSilentLogin() {
|
||||
console.log('[Main] Attempting silent login...');
|
||||
|
||||
// Notify renderer that we're trying to auto-login
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('silent-login-started');
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
silentLoginWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 700,
|
||||
show: false, // Hidden window
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
}
|
||||
});
|
||||
|
||||
silentLoginWindow.loadURL('https://claude.ai');
|
||||
|
||||
let loginCheckInterval = null;
|
||||
let hasLoggedIn = false;
|
||||
const SILENT_LOGIN_TIMEOUT = 15000; // 15 seconds timeout
|
||||
|
||||
// Function to check login status
|
||||
async function checkLoginStatus() {
|
||||
if (hasLoggedIn || !silentLoginWindow) return;
|
||||
|
||||
try {
|
||||
const cookies = await session.defaultSession.cookies.get({
|
||||
url: 'https://claude.ai',
|
||||
name: 'sessionKey'
|
||||
});
|
||||
|
||||
if (cookies.length > 0) {
|
||||
const sessionKey = cookies[0].value;
|
||||
console.log('[Main] Silent login: Session key found, attempting to get org ID...');
|
||||
|
||||
// Fetch org ID from API
|
||||
let orgId = null;
|
||||
try {
|
||||
const response = await axios.get('https://claude.ai/api/organizations', {
|
||||
headers: {
|
||||
'Cookie': `sessionKey=${sessionKey}`,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||
orgId = response.data[0].uuid || response.data[0].id;
|
||||
console.log('[Main] Silent login: Org ID fetched from API:', orgId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[Main] Silent login: API not ready yet:', err.message);
|
||||
}
|
||||
|
||||
if (sessionKey && orgId) {
|
||||
hasLoggedIn = true;
|
||||
if (loginCheckInterval) {
|
||||
clearInterval(loginCheckInterval);
|
||||
loginCheckInterval = null;
|
||||
}
|
||||
|
||||
console.log('[Main] Silent login successful!');
|
||||
store.set('sessionKey', sessionKey);
|
||||
store.set('organizationId', orgId);
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('login-success', { sessionKey, organizationId: orgId });
|
||||
}
|
||||
|
||||
silentLoginWindow.close();
|
||||
resolve(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Main] Silent login check error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Check on page load
|
||||
silentLoginWindow.webContents.on('did-finish-load', async () => {
|
||||
const url = silentLoginWindow.webContents.getURL();
|
||||
console.log('[Main] Silent login page loaded:', url);
|
||||
|
||||
if (url.includes('claude.ai')) {
|
||||
await checkLoginStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// Also check on navigation
|
||||
silentLoginWindow.webContents.on('did-navigate', async (event, url) => {
|
||||
console.log('[Main] Silent login navigated to:', url);
|
||||
if (url.includes('claude.ai')) {
|
||||
await checkLoginStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// Poll periodically
|
||||
loginCheckInterval = setInterval(async () => {
|
||||
if (!hasLoggedIn && silentLoginWindow) {
|
||||
await checkLoginStatus();
|
||||
} else if (loginCheckInterval) {
|
||||
clearInterval(loginCheckInterval);
|
||||
loginCheckInterval = null;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Timeout - if silent login doesn't work, fall back to visible login
|
||||
setTimeout(() => {
|
||||
if (!hasLoggedIn) {
|
||||
console.log('[Main] Silent login timeout, falling back to visible login...');
|
||||
if (loginCheckInterval) {
|
||||
clearInterval(loginCheckInterval);
|
||||
loginCheckInterval = null;
|
||||
}
|
||||
if (silentLoginWindow) {
|
||||
silentLoginWindow.close();
|
||||
}
|
||||
|
||||
// Notify renderer that silent login failed
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('silent-login-failed');
|
||||
}
|
||||
|
||||
// Open visible login window
|
||||
createLoginWindow();
|
||||
resolve(false);
|
||||
}
|
||||
}, SILENT_LOGIN_TIMEOUT);
|
||||
|
||||
silentLoginWindow.on('closed', () => {
|
||||
if (loginCheckInterval) {
|
||||
clearInterval(loginCheckInterval);
|
||||
loginCheckInterval = null;
|
||||
}
|
||||
silentLoginWindow = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
try {
|
||||
tray = new Tray(path.join(__dirname, 'assets/tray-icon.png'));
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show Widget',
|
||||
click: () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.show();
|
||||
} else {
|
||||
createMainWindow();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Refresh',
|
||||
click: () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('refresh-usage');
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Settings',
|
||||
click: () => {
|
||||
createSettingsWindow();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Re-login',
|
||||
click: () => {
|
||||
store.delete('sessionKey');
|
||||
store.delete('organizationId');
|
||||
createLoginWindow();
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Exit',
|
||||
click: () => {
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
tray.setToolTip('Claude Usage Widget');
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
tray.on('click', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create tray:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function createSettingsWindow() {
|
||||
if (settingsWindow) {
|
||||
settingsWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
settingsWindow = new BrowserWindow({
|
||||
width: SETTINGS_WIDTH,
|
||||
height: SETTINGS_HEIGHT,
|
||||
title: 'Settings - Claude Usage Widget',
|
||||
frame: false,
|
||||
resizable: true,
|
||||
minimizable: true,
|
||||
maximizable: false,
|
||||
icon: path.join(__dirname, 'assets/icon.ico'),
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
settingsWindow.loadFile('src/renderer/settings.html');
|
||||
|
||||
settingsWindow.on('closed', () => {
|
||||
settingsWindow = null;
|
||||
});
|
||||
|
||||
// Development tools
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
settingsWindow.webContents.openDevTools({ mode: 'detach' });
|
||||
}
|
||||
}
|
||||
|
||||
// IPC Handlers
|
||||
ipcMain.handle('get-credentials', () => {
|
||||
return {
|
||||
sessionKey: store.get('sessionKey'),
|
||||
organizationId: store.get('organizationId')
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle('save-credentials', (event, { sessionKey, organizationId }) => {
|
||||
store.set('sessionKey', sessionKey);
|
||||
if (organizationId) {
|
||||
store.set('organizationId', organizationId);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-credentials', async () => {
|
||||
store.delete('sessionKey');
|
||||
store.delete('organizationId');
|
||||
|
||||
// Clear the session cookie to ensure actual logout
|
||||
try {
|
||||
await session.defaultSession.cookies.remove('https://claude.ai', 'sessionKey');
|
||||
// Also try checking for other auth cookies or clear storage if needed
|
||||
// await session.defaultSession.clearStorageData({ storages: ['cookies'] });
|
||||
} catch (error) {
|
||||
console.error('Failed to clear cookies:', error);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.on('open-login', () => {
|
||||
createLoginWindow();
|
||||
});
|
||||
|
||||
ipcMain.on('open-settings', () => {
|
||||
createSettingsWindow();
|
||||
});
|
||||
|
||||
ipcMain.on('minimize-window', () => {
|
||||
if (mainWindow) mainWindow.hide();
|
||||
});
|
||||
|
||||
ipcMain.on('close-window', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
ipcMain.handle('get-window-position', () => {
|
||||
if (mainWindow) {
|
||||
return mainWindow.getBounds();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
ipcMain.handle('set-window-position', (event, { x, y }) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.setPosition(x, y);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
ipcMain.on('open-external', (event, url) => {
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
// Color preferences handlers
|
||||
const DEFAULT_COLOR_PREFERENCES = {
|
||||
normal: { start: '#8b5cf6', end: '#a78bfa' },
|
||||
warning: { start: '#f59e0b', end: '#fbbf24' },
|
||||
danger: { start: '#ef4444', end: '#f87171' }
|
||||
};
|
||||
|
||||
ipcMain.handle('get-color-preferences', () => {
|
||||
const saved = store.get('colorPreferences');
|
||||
return saved || DEFAULT_COLOR_PREFERENCES;
|
||||
});
|
||||
|
||||
ipcMain.handle('set-color-preferences', (event, preferences) => {
|
||||
store.set('colorPreferences', preferences);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('notify-color-change', (event, preferences) => {
|
||||
// Notify main window to update colors
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('colors-changed', preferences);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('fetch-usage-data', async () => {
|
||||
console.log('[Main] fetch-usage-data handler called');
|
||||
const sessionKey = store.get('sessionKey');
|
||||
const organizationId = store.get('organizationId');
|
||||
|
||||
console.log('[Main] Credentials:', {
|
||||
hasSessionKey: !!sessionKey,
|
||||
organizationId
|
||||
});
|
||||
|
||||
if (!sessionKey || !organizationId) {
|
||||
throw new Error('Missing credentials');
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[Main] Making API request to:', `https://claude.ai/api/organizations/${organizationId}/usage`);
|
||||
const response = await axios.get(
|
||||
`https://claude.ai/api/organizations/${organizationId}/usage`,
|
||||
{
|
||||
headers: {
|
||||
'Cookie': `sessionKey=${sessionKey}`,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log('[Main] API request successful, status:', response.status);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('[Main] API request failed:', error.message);
|
||||
if (error.response) {
|
||||
console.error('[Main] Response status:', error.response.status);
|
||||
if (error.response.status === 401 || error.response.status === 403) {
|
||||
// Session expired - attempt silent re-login
|
||||
console.log('[Main] Session expired, attempting silent re-login...');
|
||||
store.delete('sessionKey');
|
||||
store.delete('organizationId');
|
||||
|
||||
// Don't clear cookies - we need them for silent login to work with OAuth
|
||||
// The silent login will use existing Google/OAuth session if available
|
||||
|
||||
// Attempt silent login (will notify renderer appropriately)
|
||||
attemptSilentLogin();
|
||||
|
||||
throw new Error('SessionExpired');
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// App lifecycle
|
||||
app.whenReady().then(() => {
|
||||
createMainWindow();
|
||||
createTray();
|
||||
|
||||
// Check if we have credentials
|
||||
// const hasCredentials = store.get('sessionKey') && store.get('organizationId');
|
||||
// if (!hasCredentials) {
|
||||
// setTimeout(() => {
|
||||
// createLoginWindow();
|
||||
// }, 1000);
|
||||
// }
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// Don't quit on macOS
|
||||
if (process.platform !== 'darwin') {
|
||||
// Keep running in tray
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow === null) {
|
||||
createMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent multiple instances
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
50
package.json
Normal file
50
package.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "claude-usage-widget",
|
||||
"version": "1.3.0",
|
||||
"description": "Desktop widget for Claude.ai usage monitoring",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder",
|
||||
"build:win": "electron-builder --win",
|
||||
"dev": "cross-env NODE_ENV=development electron ."
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
"usage",
|
||||
"widget",
|
||||
"electron"
|
||||
],
|
||||
"author": "Your Name",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^28.0.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-store": "^8.1.0",
|
||||
"axios": "^1.6.2"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.claudeusage.widget",
|
||||
"productName": "Claude Usage Widget",
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis"
|
||||
],
|
||||
"icon": "assets/icon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true
|
||||
},
|
||||
"files": [
|
||||
"**/*",
|
||||
"!**/*.ts",
|
||||
"!*.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
49
preload.js
Normal file
49
preload.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Expose protected methods that allow the renderer process to use
|
||||
// the ipcRenderer without exposing the entire object
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Credentials management
|
||||
getCredentials: () => ipcRenderer.invoke('get-credentials'),
|
||||
saveCredentials: (credentials) => ipcRenderer.invoke('save-credentials', credentials),
|
||||
deleteCredentials: () => ipcRenderer.invoke('delete-credentials'),
|
||||
|
||||
// Window controls
|
||||
minimizeWindow: () => ipcRenderer.send('minimize-window'),
|
||||
closeWindow: () => ipcRenderer.send('close-window'),
|
||||
openLogin: () => ipcRenderer.send('open-login'),
|
||||
openSettings: () => ipcRenderer.send('open-settings'),
|
||||
|
||||
// Window position
|
||||
getWindowPosition: () => ipcRenderer.invoke('get-window-position'),
|
||||
setWindowPosition: (position) => ipcRenderer.invoke('set-window-position', position),
|
||||
|
||||
// Event listeners
|
||||
onLoginSuccess: (callback) => {
|
||||
ipcRenderer.on('login-success', (event, data) => callback(data));
|
||||
},
|
||||
onRefreshUsage: (callback) => {
|
||||
ipcRenderer.on('refresh-usage', () => callback());
|
||||
},
|
||||
onSessionExpired: (callback) => {
|
||||
ipcRenderer.on('session-expired', () => callback());
|
||||
},
|
||||
onSilentLoginStarted: (callback) => {
|
||||
ipcRenderer.on('silent-login-started', () => callback());
|
||||
},
|
||||
onSilentLoginFailed: (callback) => {
|
||||
ipcRenderer.on('silent-login-failed', () => callback());
|
||||
},
|
||||
|
||||
// API
|
||||
fetchUsageData: () => ipcRenderer.invoke('fetch-usage-data'),
|
||||
openExternal: (url) => ipcRenderer.send('open-external', url),
|
||||
|
||||
// Color preferences
|
||||
getColorPreferences: () => ipcRenderer.invoke('get-color-preferences'),
|
||||
setColorPreferences: (preferences) => ipcRenderer.invoke('set-color-preferences', preferences),
|
||||
notifyColorChange: (preferences) => ipcRenderer.invoke('notify-color-change', preferences),
|
||||
onColorsChanged: (callback) => {
|
||||
ipcRenderer.on('colors-changed', (event, preferences) => callback(preferences));
|
||||
}
|
||||
});
|
||||
428
src/renderer/app.js
Normal file
428
src/renderer/app.js
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
// Application state
|
||||
let credentials = null;
|
||||
let updateInterval = null;
|
||||
let countdownInterval = null;
|
||||
let latestUsageData = null;
|
||||
const UPDATE_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// DOM elements
|
||||
const elements = {
|
||||
loadingContainer: document.getElementById('loadingContainer'),
|
||||
loginContainer: document.getElementById('loginContainer'),
|
||||
noUsageContainer: document.getElementById('noUsageContainer'),
|
||||
autoLoginContainer: document.getElementById('autoLoginContainer'),
|
||||
mainContent: document.getElementById('mainContent'),
|
||||
loginBtn: document.getElementById('loginBtn'),
|
||||
refreshBtn: document.getElementById('refreshBtn'),
|
||||
minimizeBtn: document.getElementById('minimizeBtn'),
|
||||
closeBtn: document.getElementById('closeBtn'),
|
||||
|
||||
sessionPercentage: document.getElementById('sessionPercentage'),
|
||||
sessionProgress: document.getElementById('sessionProgress'),
|
||||
sessionTimer: document.getElementById('sessionTimer'),
|
||||
sessionTimeText: document.getElementById('sessionTimeText'),
|
||||
|
||||
weeklyPercentage: document.getElementById('weeklyPercentage'),
|
||||
weeklyProgress: document.getElementById('weeklyProgress'),
|
||||
weeklyTimer: document.getElementById('weeklyTimer'),
|
||||
weeklyTimeText: document.getElementById('weeklyTimeText'),
|
||||
|
||||
settingsBtn: document.getElementById('settingsBtn')
|
||||
};
|
||||
|
||||
// Initialize
|
||||
async function init() {
|
||||
setupEventListeners();
|
||||
credentials = await window.electronAPI.getCredentials();
|
||||
|
||||
// Load and apply color preferences
|
||||
const colorPrefs = await window.electronAPI.getColorPreferences();
|
||||
applyColorPreferences(colorPrefs);
|
||||
|
||||
if (credentials.sessionKey && credentials.organizationId) {
|
||||
showMainContent();
|
||||
await fetchUsageData();
|
||||
startAutoUpdate();
|
||||
} else {
|
||||
showLoginRequired();
|
||||
}
|
||||
}
|
||||
|
||||
// Event Listeners
|
||||
function setupEventListeners() {
|
||||
elements.loginBtn.addEventListener('click', () => {
|
||||
window.electronAPI.openLogin();
|
||||
});
|
||||
|
||||
elements.refreshBtn.addEventListener('click', async () => {
|
||||
console.log('Refresh button clicked');
|
||||
elements.refreshBtn.classList.add('spinning');
|
||||
await fetchUsageData();
|
||||
elements.refreshBtn.classList.remove('spinning');
|
||||
});
|
||||
|
||||
elements.minimizeBtn.addEventListener('click', () => {
|
||||
window.electronAPI.minimizeWindow();
|
||||
});
|
||||
|
||||
elements.closeBtn.addEventListener('click', () => {
|
||||
window.electronAPI.closeWindow(); // Exit application completely
|
||||
});
|
||||
|
||||
// Settings button
|
||||
elements.settingsBtn.addEventListener('click', () => {
|
||||
window.electronAPI.openSettings();
|
||||
});
|
||||
|
||||
// Listen for login success
|
||||
window.electronAPI.onLoginSuccess(async (data) => {
|
||||
console.log('Renderer received login-success event', data);
|
||||
credentials = data;
|
||||
await window.electronAPI.saveCredentials(data);
|
||||
console.log('Credentials saved, showing main content');
|
||||
showMainContent();
|
||||
await fetchUsageData();
|
||||
startAutoUpdate();
|
||||
});
|
||||
|
||||
// Listen for refresh requests from tray
|
||||
window.electronAPI.onRefreshUsage(async () => {
|
||||
await fetchUsageData();
|
||||
});
|
||||
|
||||
// Listen for session expiration events (403 errors) - only used as fallback
|
||||
window.electronAPI.onSessionExpired(() => {
|
||||
console.log('Session expired event received');
|
||||
credentials = { sessionKey: null, organizationId: null };
|
||||
showLoginRequired();
|
||||
});
|
||||
|
||||
// Listen for silent login attempts
|
||||
window.electronAPI.onSilentLoginStarted(() => {
|
||||
console.log('Silent login started...');
|
||||
showAutoLoginAttempt();
|
||||
});
|
||||
|
||||
// Listen for silent login failures (falls back to visible login)
|
||||
window.electronAPI.onSilentLoginFailed(() => {
|
||||
console.log('Silent login failed, manual login required');
|
||||
showLoginRequired();
|
||||
});
|
||||
|
||||
// Listen for color preference changes from settings window
|
||||
window.electronAPI.onColorsChanged((preferences) => {
|
||||
console.log('Colors changed, applying new preferences');
|
||||
applyColorPreferences(preferences);
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch usage data from Claude API
|
||||
async function fetchUsageData() {
|
||||
console.log('fetchUsageData called', { credentials });
|
||||
|
||||
if (!credentials.sessionKey || !credentials.organizationId) {
|
||||
console.log('Missing credentials, showing login');
|
||||
showLoginRequired();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Calling electronAPI.fetchUsageData...');
|
||||
const data = await window.electronAPI.fetchUsageData();
|
||||
console.log('Received usage data:', data);
|
||||
updateUI(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching usage data:', error);
|
||||
if (error.message.includes('SessionExpired') || error.message.includes('Unauthorized')) {
|
||||
// Session expired - silent login attempt is in progress
|
||||
// Show auto-login UI while waiting
|
||||
credentials = { sessionKey: null, organizationId: null };
|
||||
showAutoLoginAttempt();
|
||||
} else {
|
||||
showError('Failed to fetch usage data');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's no usage data
|
||||
function hasNoUsage(data) {
|
||||
const sessionUtilization = data.five_hour?.utilization || 0;
|
||||
const sessionResetsAt = data.five_hour?.resets_at;
|
||||
const weeklyUtilization = data.seven_day?.utilization || 0;
|
||||
const weeklyResetsAt = data.seven_day?.resets_at;
|
||||
|
||||
return sessionUtilization === 0 && !sessionResetsAt &&
|
||||
weeklyUtilization === 0 && !weeklyResetsAt;
|
||||
}
|
||||
|
||||
// Update UI with usage data
|
||||
function updateUI(data) {
|
||||
latestUsageData = data;
|
||||
|
||||
// Check if there's no usage data
|
||||
if (hasNoUsage(data)) {
|
||||
showNoUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
showMainContent();
|
||||
refreshTimers();
|
||||
startCountdown();
|
||||
}
|
||||
|
||||
// Track if we've already triggered a refresh for expired timers
|
||||
let sessionResetTriggered = false;
|
||||
let weeklyResetTriggered = false;
|
||||
|
||||
function refreshTimers() {
|
||||
if (!latestUsageData) return;
|
||||
|
||||
// Session data
|
||||
const sessionUtilization = latestUsageData.five_hour?.utilization || 0;
|
||||
const sessionResetsAt = latestUsageData.five_hour?.resets_at;
|
||||
|
||||
// Check if session timer has expired and we need to refresh
|
||||
if (sessionResetsAt) {
|
||||
const sessionDiff = new Date(sessionResetsAt) - new Date();
|
||||
if (sessionDiff <= 0 && !sessionResetTriggered) {
|
||||
sessionResetTriggered = true;
|
||||
console.log('Session timer expired, triggering refresh...');
|
||||
// Wait a few seconds for the server to update, then refresh
|
||||
setTimeout(() => {
|
||||
fetchUsageData();
|
||||
}, 3000);
|
||||
} else if (sessionDiff > 0) {
|
||||
sessionResetTriggered = false; // Reset flag when timer is active again
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressBar(
|
||||
elements.sessionProgress,
|
||||
elements.sessionPercentage,
|
||||
sessionUtilization
|
||||
);
|
||||
|
||||
updateTimer(
|
||||
elements.sessionTimer,
|
||||
elements.sessionTimeText,
|
||||
sessionResetsAt,
|
||||
5 * 60 // 5 hours in minutes
|
||||
);
|
||||
|
||||
// Weekly data
|
||||
const weeklyUtilization = latestUsageData.seven_day?.utilization || 0;
|
||||
const weeklyResetsAt = latestUsageData.seven_day?.resets_at;
|
||||
|
||||
// Check if weekly timer has expired and we need to refresh
|
||||
if (weeklyResetsAt) {
|
||||
const weeklyDiff = new Date(weeklyResetsAt) - new Date();
|
||||
if (weeklyDiff <= 0 && !weeklyResetTriggered) {
|
||||
weeklyResetTriggered = true;
|
||||
console.log('Weekly timer expired, triggering refresh...');
|
||||
setTimeout(() => {
|
||||
fetchUsageData();
|
||||
}, 3000);
|
||||
} else if (weeklyDiff > 0) {
|
||||
weeklyResetTriggered = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressBar(
|
||||
elements.weeklyProgress,
|
||||
elements.weeklyPercentage,
|
||||
weeklyUtilization,
|
||||
true
|
||||
);
|
||||
|
||||
updateTimer(
|
||||
elements.weeklyTimer,
|
||||
elements.weeklyTimeText,
|
||||
weeklyResetsAt,
|
||||
7 * 24 * 60 // 7 days in minutes
|
||||
);
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
countdownInterval = setInterval(() => {
|
||||
refreshTimers();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
function updateProgressBar(progressElement, percentageElement, value, isWeekly = false) {
|
||||
const percentage = Math.min(Math.max(value, 0), 100);
|
||||
|
||||
progressElement.style.width = `${percentage}%`;
|
||||
percentageElement.textContent = `${Math.round(percentage)}%`;
|
||||
|
||||
// Update color based on usage level
|
||||
progressElement.classList.remove('warning', 'danger');
|
||||
if (percentage >= 90) {
|
||||
progressElement.classList.add('danger');
|
||||
} else if (percentage >= 75) {
|
||||
progressElement.classList.add('warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Update circular timer
|
||||
function updateTimer(timerElement, textElement, resetsAt, totalMinutes) {
|
||||
if (!resetsAt) {
|
||||
textElement.textContent = '--:--';
|
||||
textElement.style.opacity = '0.5';
|
||||
textElement.title = 'Starts when a message is sent';
|
||||
timerElement.style.strokeDashoffset = 63;
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the greyed out styling and tooltip when timer is active
|
||||
textElement.style.opacity = '1';
|
||||
textElement.title = '';
|
||||
|
||||
const resetDate = new Date(resetsAt);
|
||||
const now = new Date();
|
||||
const diff = resetDate - now;
|
||||
|
||||
if (diff <= 0) {
|
||||
textElement.textContent = 'Resetting...';
|
||||
timerElement.style.strokeDashoffset = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate remaining time
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
// const seconds = Math.floor((diff % (1000 * 60)) / 1000); // Optional seconds
|
||||
|
||||
// Format time display
|
||||
if (hours >= 24) {
|
||||
const days = Math.floor(hours / 24);
|
||||
const remainingHours = hours % 24;
|
||||
textElement.textContent = `${days}d ${remainingHours}h`;
|
||||
} else if (hours > 0) {
|
||||
textElement.textContent = `${hours}h ${minutes}m`;
|
||||
} else {
|
||||
textElement.textContent = `${minutes}m`;
|
||||
}
|
||||
|
||||
// Calculate progress (elapsed percentage)
|
||||
const totalMs = totalMinutes * 60 * 1000;
|
||||
const elapsedMs = totalMs - diff;
|
||||
const elapsedPercentage = (elapsedMs / totalMs) * 100;
|
||||
|
||||
// Update circle (63 is ~2*pi*10)
|
||||
const circumference = 63;
|
||||
const offset = circumference - (elapsedPercentage / 100) * circumference;
|
||||
timerElement.style.strokeDashoffset = offset;
|
||||
|
||||
// Update color based on remaining time
|
||||
timerElement.classList.remove('warning', 'danger');
|
||||
if (elapsedPercentage >= 90) {
|
||||
timerElement.classList.add('danger');
|
||||
} else if (elapsedPercentage >= 75) {
|
||||
timerElement.classList.add('warning');
|
||||
}
|
||||
}
|
||||
|
||||
// UI State Management
|
||||
function showLoading() {
|
||||
elements.loadingContainer.style.display = 'block';
|
||||
elements.loginContainer.style.display = 'none';
|
||||
elements.noUsageContainer.style.display = 'none';
|
||||
elements.autoLoginContainer.style.display = 'none';
|
||||
elements.mainContent.style.display = 'none';
|
||||
}
|
||||
|
||||
function showLoginRequired() {
|
||||
elements.loadingContainer.style.display = 'none';
|
||||
elements.loginContainer.style.display = 'flex'; // Use flex to preserve centering
|
||||
elements.noUsageContainer.style.display = 'none';
|
||||
elements.autoLoginContainer.style.display = 'none';
|
||||
elements.mainContent.style.display = 'none';
|
||||
stopAutoUpdate();
|
||||
}
|
||||
|
||||
function showNoUsage() {
|
||||
elements.loadingContainer.style.display = 'none';
|
||||
elements.loginContainer.style.display = 'none';
|
||||
elements.noUsageContainer.style.display = 'flex';
|
||||
elements.autoLoginContainer.style.display = 'none';
|
||||
elements.mainContent.style.display = 'none';
|
||||
}
|
||||
|
||||
function showAutoLoginAttempt() {
|
||||
elements.loadingContainer.style.display = 'none';
|
||||
elements.loginContainer.style.display = 'none';
|
||||
elements.noUsageContainer.style.display = 'none';
|
||||
elements.autoLoginContainer.style.display = 'flex';
|
||||
elements.mainContent.style.display = 'none';
|
||||
stopAutoUpdate();
|
||||
}
|
||||
|
||||
function showMainContent() {
|
||||
elements.loadingContainer.style.display = 'none';
|
||||
elements.loginContainer.style.display = 'none';
|
||||
elements.noUsageContainer.style.display = 'none';
|
||||
elements.autoLoginContainer.style.display = 'none';
|
||||
elements.mainContent.style.display = 'block';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
// TODO: Implement error notification
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
// Color preference management
|
||||
function applyColorPreferences(prefs) {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Apply normal colors
|
||||
root.style.setProperty('--color-normal-start', prefs.normal.start);
|
||||
root.style.setProperty('--color-normal-end', prefs.normal.end);
|
||||
|
||||
// Apply warning colors
|
||||
root.style.setProperty('--color-warning-start', prefs.warning.start);
|
||||
root.style.setProperty('--color-warning-end', prefs.warning.end);
|
||||
|
||||
// Apply danger colors
|
||||
root.style.setProperty('--color-danger-start', prefs.danger.start);
|
||||
root.style.setProperty('--color-danger-end', prefs.danger.end);
|
||||
}
|
||||
|
||||
// Auto-update management
|
||||
function startAutoUpdate() {
|
||||
stopAutoUpdate();
|
||||
updateInterval = setInterval(() => {
|
||||
fetchUsageData();
|
||||
}, UPDATE_INTERVAL);
|
||||
}
|
||||
|
||||
function stopAutoUpdate() {
|
||||
if (updateInterval) {
|
||||
clearInterval(updateInterval);
|
||||
updateInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Add spinning animation for refresh button
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes spin-refresh {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.refresh-btn.spinning svg {
|
||||
animation: spin-refresh 1s linear;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Start the application
|
||||
init();
|
||||
|
||||
// Cleanup on unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
stopAutoUpdate();
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
});
|
||||
129
src/renderer/index.html
Normal file
129
src/renderer/index.html
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>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="styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="widget-container" id="widgetContainer">
|
||||
<!-- Title Bar -->
|
||||
<div class="title-bar" id="titleBar">
|
||||
<div class="title">
|
||||
<img src="../../assets/logo.png" alt="Logo" class="app-logo">
|
||||
<span>Claude Usage</span>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button class="control-btn settings-btn" id="settingsBtn" title="Settings">⚙️</button>
|
||||
<button class="control-btn refresh-btn" id="refreshBtn" title="Refresh">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M23 4v6h-6M1 20v-6h6" />
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="control-btn minimize-btn" id="minimizeBtn" title="Minimise to System Tray">−</button>
|
||||
<button class="control-btn close-btn" id="closeBtn" title="Exit Application">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div class="loading-container" id="loadingContainer">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading usage data...</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Required State -->
|
||||
<div class="login-container" id="loginContainer" style="display: none;">
|
||||
<div class="login-content">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
|
||||
<polyline points="10 17 15 12 10 7" />
|
||||
<line x1="15" y1="12" x2="3" y2="12" />
|
||||
</svg>
|
||||
<div class="login-text-group">
|
||||
<h3>Login Required</h3>
|
||||
<p>Log in to view usage stats</p>
|
||||
</div>
|
||||
<button class="login-btn" id="loginBtn">Log In</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Usage State -->
|
||||
<div class="no-usage-container" id="noUsageContainer" style="display: none;">
|
||||
<div class="no-usage-content">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<div class="no-usage-text-group">
|
||||
<h3>No Usage Yet</h3>
|
||||
<p>Start chatting with Claude to see your usage stats</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto-Login Attempt State -->
|
||||
<div class="auto-login-container" id="autoLoginContainer" style="display: none;">
|
||||
<div class="auto-login-content">
|
||||
<div class="spinner"></div>
|
||||
<div class="auto-login-text-group">
|
||||
<h3>Trying to auto-login...</h3>
|
||||
<p>Please wait while we reconnect</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="content" id="mainContent" style="display: none;">
|
||||
<!-- Session Usage -->
|
||||
<div class="usage-section">
|
||||
<span class="usage-label">Current Session</span>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="sessionProgress" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="usage-percentage" id="sessionPercentage">0%</span>
|
||||
<div class="timer-container">
|
||||
<div class="timer-text" id="sessionTimeText">--:--</div>
|
||||
<svg class="mini-timer" width="24" height="24" viewBox="0 0 24 24">
|
||||
<circle class="timer-bg" cx="12" cy="12" r="10" />
|
||||
<circle class="timer-progress" id="sessionTimer" cx="12" cy="12" r="10"
|
||||
style="stroke-dasharray: 63; stroke-dashoffset: 63" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Weekly Usage -->
|
||||
<div class="usage-section">
|
||||
<span class="usage-label">Weekly Limit</span>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill weekly" id="weeklyProgress" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="usage-percentage" id="weeklyPercentage">0%</span>
|
||||
<div class="timer-container">
|
||||
<div class="timer-text" id="weeklyTimeText">--:--</div>
|
||||
<svg class="mini-timer" width="24" height="24" viewBox="0 0 24 24">
|
||||
<circle class="timer-bg" cx="12" cy="12" r="10" />
|
||||
<circle class="timer-progress weekly" id="weeklyTimer" cx="12" cy="12" r="10"
|
||||
style="stroke-dasharray: 63; stroke-dashoffset: 63" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Footer element (removed) -->
|
||||
<span id="lastUpdate" style="display: none;"></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
230
src/renderer/settings-styles.css
Normal file
230
src/renderer/settings-styles.css
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Title Bar */
|
||||
.title-bar {
|
||||
-webkit-app-region: drag;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 8px 12px;
|
||||
padding-left: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #e0e0e0;
|
||||
font-family: 'Libre Baskerville', serif;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
border-radius: 6px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.controls {
|
||||
-webkit-app-region: no-drag;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-right: -10px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #a0a0a0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#settings-app {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.settings-section h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 2-Column Grid for Color Pickers */
|
||||
.color-grid-2col {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.color-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.color-item label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #a0a0a0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.color-item input[type="color"] {
|
||||
width: 80px;
|
||||
height: 50px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.color-item input[type="color"]:hover {
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.color-item input[type="color"]::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.color-item input[type="color"]::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-secondary,
|
||||
.btn-danger,
|
||||
.btn-coffee {
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #e0e0e0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: transparent;
|
||||
color: #ef4444;
|
||||
border: 1px solid rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border-color: rgba(239, 68, 68, 0.7);
|
||||
}
|
||||
|
||||
.btn-coffee {
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.btn-coffee:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(255, 165, 0, 0.4);
|
||||
}
|
||||
|
||||
.btn-coffee svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
margin: 24px 0;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Disclaimer */
|
||||
.disclaimer {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.disclaimer strong {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Disclaimer spacing */
|
||||
.disclaimer {
|
||||
margin-top: 16px;
|
||||
}
|
||||
105
src/renderer/settings.html
Normal file
105
src/renderer/settings.html
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings - Claude Usage Widget</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="settings-styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="settings-app">
|
||||
<!-- Title Bar -->
|
||||
<div class="title-bar" id="titleBar">
|
||||
<div class="title">
|
||||
<img src="../../assets/logo.png" alt="Logo" class="app-logo">
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button class="control-btn minimize-btn" id="minimizeBtn" title="Minimize">−</button>
|
||||
<button class="control-btn close-btn" id="closeBtn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-container">
|
||||
|
||||
<!-- Color Customization -->
|
||||
<section class="settings-section">
|
||||
<h2>Customize Colors</h2>
|
||||
|
||||
<div class="color-grid-2col">
|
||||
<div class="color-item">
|
||||
<label>Normal Start</label>
|
||||
<input type="color" id="colorNormalStart" value="#8b5cf6">
|
||||
</div>
|
||||
|
||||
<div class="color-item">
|
||||
<label>Normal End</label>
|
||||
<input type="color" id="colorNormalEnd" value="#a78bfa">
|
||||
</div>
|
||||
|
||||
<div class="color-item">
|
||||
<label>Warning Start</label>
|
||||
<input type="color" id="colorWarningStart" value="#f59e0b">
|
||||
</div>
|
||||
|
||||
<div class="color-item">
|
||||
<label>Warning End</label>
|
||||
<input type="color" id="colorWarningEnd" value="#fbbf24">
|
||||
</div>
|
||||
|
||||
<div class="color-item">
|
||||
<label>Danger Start</label>
|
||||
<input type="color" id="colorDangerStart" value="#ef4444">
|
||||
</div>
|
||||
|
||||
<div class="color-item">
|
||||
<label>Danger End</label>
|
||||
<input type="color" id="colorDangerEnd" value="#f87171">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-secondary" id="resetColorsBtn">Reset to Defaults</button>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Account Actions -->
|
||||
<section class="settings-section">
|
||||
<h2>Account</h2>
|
||||
<button class="btn-danger" id="logoutBtn">Log Out</button>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Support -->
|
||||
<section class="settings-section">
|
||||
<button class="btn-coffee" id="coffeeBtn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 8h1a4 4 0 1 1 0 8h-1" />
|
||||
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z" />
|
||||
<line x1="6" y1="2" x2="6" y2="4" />
|
||||
<line x1="10" y1="2" x2="10" y2="4" />
|
||||
<line x1="14" y1="2" x2="14" y2="4" />
|
||||
</svg>
|
||||
If you find this useful, buy me a coffee!
|
||||
</button>
|
||||
|
||||
<!-- Disclaimer -->
|
||||
<p class="disclaimer">
|
||||
<strong>Disclaimer:</strong> Unofficial tool not affiliated with Anthropic. Use at your own
|
||||
discretion.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="settings.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
107
src/renderer/settings.js
Normal file
107
src/renderer/settings.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// DOM elements
|
||||
const elements = {
|
||||
colorNormalStart: document.getElementById('colorNormalStart'),
|
||||
colorNormalEnd: document.getElementById('colorNormalEnd'),
|
||||
colorWarningStart: document.getElementById('colorWarningStart'),
|
||||
colorWarningEnd: document.getElementById('colorWarningEnd'),
|
||||
colorDangerStart: document.getElementById('colorDangerStart'),
|
||||
colorDangerEnd: document.getElementById('colorDangerEnd'),
|
||||
resetColorsBtn: document.getElementById('resetColorsBtn'),
|
||||
logoutBtn: document.getElementById('logoutBtn'),
|
||||
coffeeBtn: document.getElementById('coffeeBtn'),
|
||||
minimizeBtn: document.getElementById('minimizeBtn'),
|
||||
closeBtn: document.getElementById('closeBtn')
|
||||
};
|
||||
|
||||
// Initialize
|
||||
async function init() {
|
||||
setupEventListeners();
|
||||
|
||||
// Load and apply color preferences
|
||||
const colorPrefs = await window.electronAPI.getColorPreferences();
|
||||
loadColorPickerValues(colorPrefs);
|
||||
}
|
||||
|
||||
// Event Listeners
|
||||
function setupEventListeners() {
|
||||
// Color picker event listeners
|
||||
const colorInputs = [
|
||||
elements.colorNormalStart,
|
||||
elements.colorNormalEnd,
|
||||
elements.colorWarningStart,
|
||||
elements.colorWarningEnd,
|
||||
elements.colorDangerStart,
|
||||
elements.colorDangerEnd
|
||||
];
|
||||
|
||||
colorInputs.forEach(input => {
|
||||
input.addEventListener('change', async () => {
|
||||
await saveCurrentColors();
|
||||
});
|
||||
});
|
||||
|
||||
elements.resetColorsBtn.addEventListener('click', async () => {
|
||||
const defaults = {
|
||||
normal: { start: '#8b5cf6', end: '#a78bfa' },
|
||||
warning: { start: '#f59e0b', end: '#fbbf24' },
|
||||
danger: { start: '#ef4444', end: '#f87171' }
|
||||
};
|
||||
await window.electronAPI.setColorPreferences(defaults);
|
||||
loadColorPickerValues(defaults);
|
||||
// Notify main window to update colors immediately
|
||||
await window.electronAPI.notifyColorChange(defaults);
|
||||
});
|
||||
|
||||
elements.logoutBtn.addEventListener('click', async () => {
|
||||
await window.electronAPI.deleteCredentials();
|
||||
window.close();
|
||||
window.electronAPI.openLogin();
|
||||
});
|
||||
|
||||
elements.coffeeBtn.addEventListener('click', () => {
|
||||
window.electronAPI.openExternal('https://paypal.me/SlavomirDurej?country.x=GB&locale.x=en_GB');
|
||||
});
|
||||
|
||||
// Window controls
|
||||
elements.minimizeBtn.addEventListener('click', () => {
|
||||
window.electronAPI.minimizeWindow();
|
||||
});
|
||||
|
||||
elements.closeBtn.addEventListener('click', () => {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function loadColorPickerValues(prefs) {
|
||||
elements.colorNormalStart.value = prefs.normal.start;
|
||||
elements.colorNormalEnd.value = prefs.normal.end;
|
||||
elements.colorWarningStart.value = prefs.warning.start;
|
||||
elements.colorWarningEnd.value = prefs.warning.end;
|
||||
elements.colorDangerStart.value = prefs.danger.start;
|
||||
elements.colorDangerEnd.value = prefs.danger.end;
|
||||
}
|
||||
|
||||
async function saveCurrentColors() {
|
||||
const prefs = {
|
||||
normal: {
|
||||
start: elements.colorNormalStart.value,
|
||||
end: elements.colorNormalEnd.value
|
||||
},
|
||||
warning: {
|
||||
start: elements.colorWarningStart.value,
|
||||
end: elements.colorWarningEnd.value
|
||||
},
|
||||
danger: {
|
||||
start: elements.colorDangerStart.value,
|
||||
end: elements.colorDangerEnd.value
|
||||
}
|
||||
};
|
||||
|
||||
await window.electronAPI.setColorPreferences(prefs);
|
||||
// Notify main window to update colors
|
||||
await window.electronAPI.notifyColorChange(prefs);
|
||||
}
|
||||
|
||||
// Start the application
|
||||
init();
|
||||
589
src/renderer/styles.css
Normal file
589
src/renderer/styles.css
Normal file
|
|
@ -0,0 +1,589 @@
|
|||
:root {
|
||||
/* Customizable color scheme - normal state */
|
||||
--color-normal-start: #8b5cf6;
|
||||
--color-normal-end: #a78bfa;
|
||||
|
||||
/* Warning state (>=75% usage) */
|
||||
--color-warning-start: #f59e0b;
|
||||
--color-warning-end: #fbbf24;
|
||||
|
||||
/* Danger state (>=90% usage) */
|
||||
--color-danger-start: #ef4444;
|
||||
--color-danger-end: #f87171;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
/* Remove default focus outline */
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.widget-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.refresh-btn svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading-container {
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||
border-top-color: #8b5cf6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Login State */
|
||||
.login-container {
|
||||
padding: 0 16px;
|
||||
flex: 1;
|
||||
/* Fill remaining vertical space */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.login-content svg {
|
||||
color: #8b5cf6;
|
||||
margin-bottom: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-text-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-right: auto;
|
||||
/* Push button to right if space permits */
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.login-content h3 {
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.login-content p {
|
||||
color: #a0a0a0;
|
||||
font-size: 11px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
/* No Usage State */
|
||||
.no-usage-container {
|
||||
padding: 0 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.no-usage-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-usage-content svg {
|
||||
color: #8b5cf6;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.no-usage-text-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.no-usage-content h3 {
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.no-usage-content p {
|
||||
color: #a0a0a0;
|
||||
font-size: 11px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Auto-Login Attempt State */
|
||||
.auto-login-container {
|
||||
padding: 0 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auto-login-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auto-login-content .spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auto-login-text-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auto-login-content h3 {
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.auto-login-content p {
|
||||
color: #a0a0a0;
|
||||
font-size: 11px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.usage-section {
|
||||
display: grid;
|
||||
grid-template-columns: 125px 1fr 45px 100px;
|
||||
/* Fixed width labels for alignment */
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 2px;
|
||||
padding: 0 16px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.usage-section:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* usage-header was removed */
|
||||
|
||||
|
||||
/* We need to re-target the children of usage-header because display:contents strips the wrapper box */
|
||||
/* But the HTML structure is:
|
||||
.usage-section > .usage-header > span, span
|
||||
.usage-section > .progress-bar
|
||||
.usage-section > .timer-container
|
||||
*/
|
||||
/* Actually, let's just make usage-header NOT display contents, but hide it or remove it in HTML?
|
||||
Better: make usage-section a Flex Row?
|
||||
Or keeps Grid:
|
||||
Column 1: Label
|
||||
Column 2: Progress (flexible)
|
||||
Column 3: Percentage
|
||||
Column 4: Timer
|
||||
*/
|
||||
|
||||
.usage-label {
|
||||
color: #a0a0a0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.usage-percentage {
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
/* Remove margin as it is in grid */
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-normal-start) 0%, var(--color-normal-end) 100%);
|
||||
border-radius: 3px;
|
||||
transition: width 0.6s ease;
|
||||
position: relative;
|
||||
box-shadow: 0 0 10px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Shimmer removed */
|
||||
|
||||
.progress-fill.weekly {
|
||||
background: linear-gradient(90deg, var(--color-normal-start) 0%, var(--color-normal-end) 100%);
|
||||
box-shadow: 0 0 10px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
|
||||
/* Timer Container */
|
||||
.timer-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
/* Text on left, chart on right */
|
||||
gap: 8px;
|
||||
/* space between text and pie chart */
|
||||
padding-left: 8px;
|
||||
/* Alignment adjustment */
|
||||
}
|
||||
|
||||
.mini-timer {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.timer-bg {
|
||||
fill: none;
|
||||
stroke: rgba(255, 255, 255, 0.1);
|
||||
stroke-width: 4;
|
||||
}
|
||||
|
||||
.timer-progress {
|
||||
fill: none;
|
||||
stroke: var(--color-normal-start);
|
||||
stroke-width: 4;
|
||||
transition: stroke-dashoffset 0.6s ease;
|
||||
}
|
||||
|
||||
.timer-progress.weekly {
|
||||
stroke: var(--color-normal-start);
|
||||
}
|
||||
|
||||
.timer-text {
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Settings Overlay */
|
||||
.settings-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: 0 24px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.icon-close-settings-btn {
|
||||
position: fixed;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: none;
|
||||
color: #a0a0a0;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-close-settings-btn:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
color: #a0a0a0;
|
||||
font-size: 9px;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.coffee-btn {
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
color: #1a1a1a;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.coffee-btn svg {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.coffee-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(255, 165, 0, 0.4);
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background: transparent;
|
||||
color: #ef4444;
|
||||
border: 1px solid rgba(239, 68, 68, 0.5);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border-color: rgba(239, 68, 68, 0.7);
|
||||
}
|
||||
|
||||
.close-settings-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-settings-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.last-update {
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Scrollbar (if needed) */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Warning states */
|
||||
.progress-fill.warning {
|
||||
background: linear-gradient(90deg, var(--color-warning-start) 0%, var(--color-warning-end) 100%);
|
||||
}
|
||||
|
||||
.progress-fill.danger {
|
||||
background: linear-gradient(90deg, var(--color-danger-start) 0%, var(--color-danger-end) 100%);
|
||||
}
|
||||
|
||||
.timer-progress.warning {
|
||||
stroke: var(--color-warning-start);
|
||||
}
|
||||
|
||||
.timer-progress.danger {
|
||||
stroke: var(--color-danger-start);
|
||||
}
|
||||
Loading…
Reference in a new issue