Major Features: - Added complete UI element visibility controls (show/hide any element independently) - Added system tray icon with live usage percentage display - Added customizable tray icon colors and update interval (10-300 seconds) - Added application settings (start on boot, start minimized, close to tray) - Added theme customization (backgrounds, text colors, borders, opacity controls) - Added configurable update intervals for both UI and tray (10-300 seconds) - Added static color mode option for progress bars - Added dynamic window resizing based on visible elements UI/UX Improvements: - Improved settings organization with collapsible sections - Consistent typography and spacing throughout settings panel - Larger, bolder section headers for better visual hierarchy - Removed donations button from settings - Element visibility controls for both Current Session and Weekly Limit sections - Master toggles for entire sections - Independent toggles for labels, bars, percentages, circles, and time text Bug Fixes: - Fixed element centering and responsive layout - Fixed timer container visibility handling when children are hidden - Fixed slider overlap with text above - Fixed window resizing to properly fit content - Fixed manual resizing while maintaining centered content Technical Changes: - Changed UI update interval from 1-10 minutes to 10-300 seconds - Standardized all settings to use .setting-group wrapper class - Removed inline styles in favor of CSS classes - Improved CSS organization with consistent spacing rules - Added proper flexbox centering for responsive layouts - Window constraints: 320-600px width, 96-180px height - Resizable window with content-based auto-sizing Documentation: - Comprehensive README update with all v1.4.2 features - Added screenshot placeholders for user documentation - Detailed settings explanations - Troubleshooting section expanded - Version history updated 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
635 lines
23 KiB
JavaScript
635 lines
23 KiB
JavaScript
// 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);
|
|
|
|
// Load and apply theme settings
|
|
const themeSettings = await window.electronAPI.getThemeSettings();
|
|
applyThemePreferences(themeSettings);
|
|
|
|
// Load and apply UI visibility settings
|
|
const uiVisibility = await window.electronAPI.getUIVisibility();
|
|
applyUIVisibility(uiVisibility);
|
|
|
|
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);
|
|
});
|
|
|
|
// Listen for theme changes from settings window
|
|
window.electronAPI.onThemeChanged((theme) => {
|
|
console.log('Theme changed, applying new theme');
|
|
applyThemePreferences(theme);
|
|
});
|
|
|
|
// Listen for UI visibility changes from settings window
|
|
window.electronAPI.onUIVisibilityChanged((visibility) => {
|
|
console.log('UI visibility changed, applying new settings');
|
|
applyUIVisibility(visibility);
|
|
});
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Send usage data to main process for tray icon
|
|
if (window.electronAPI && window.electronAPI.sendUsageToMain) {
|
|
window.electronAPI.sendUsageToMain(data);
|
|
}
|
|
|
|
// Check if there's no usage data
|
|
if (hasNoUsage(data)) {
|
|
showNoUsage();
|
|
return;
|
|
}
|
|
|
|
showMainContent();
|
|
refreshTimers();
|
|
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);
|
|
}
|
|
|
|
// Theme preference management
|
|
function applyThemePreferences(theme) {
|
|
const root = document.documentElement;
|
|
const widgetContainer = document.querySelector('.widget-container');
|
|
const titleBar = document.querySelector('.title-bar');
|
|
|
|
// Apply background gradient
|
|
if (widgetContainer) {
|
|
widgetContainer.style.background = `linear-gradient(135deg, ${theme.backgroundStart} 0%, ${theme.backgroundEnd} 100%)`;
|
|
}
|
|
|
|
// Apply text colors
|
|
root.style.setProperty('--text-primary', theme.textPrimary);
|
|
root.style.setProperty('--text-secondary', theme.textSecondary);
|
|
|
|
// Apply title bar background
|
|
if (titleBar) {
|
|
const opacity = theme.titleBarOpacity / 100;
|
|
const bgColor = theme.titleBarBg;
|
|
titleBar.style.background = `rgba(${parseInt(bgColor.slice(1, 3), 16)}, ${parseInt(bgColor.slice(3, 5), 16)}, ${parseInt(bgColor.slice(5, 7), 16)}, ${opacity})`;
|
|
}
|
|
|
|
// Apply border color
|
|
if (widgetContainer) {
|
|
const borderOpacity = theme.borderOpacity / 100;
|
|
const borderColor = theme.borderColor;
|
|
widgetContainer.style.borderColor = `rgba(${parseInt(borderColor.slice(1, 3), 16)}, ${parseInt(borderColor.slice(3, 5), 16)}, ${parseInt(borderColor.slice(5, 7), 16)}, ${borderOpacity})`;
|
|
}
|
|
}
|
|
|
|
// UI visibility management
|
|
function applyUIVisibility(visibility) {
|
|
const usageSections = document.querySelectorAll('.usage-section');
|
|
if (usageSections.length < 2) return;
|
|
|
|
const sessionSection = usageSections[0];
|
|
const weeklySection = usageSections[1];
|
|
|
|
// Default to true if not explicitly set to false
|
|
const showSessionSection = visibility.showSessionSection !== false;
|
|
const showWeeklySection = visibility.showWeeklySection !== false;
|
|
const sessionShowLabel = visibility.sessionShowLabel !== false;
|
|
const sessionShowBar = visibility.sessionShowBar !== false;
|
|
const sessionShowPercentage = visibility.sessionShowPercentage !== false;
|
|
const sessionShowCircle = visibility.sessionShowCircle !== false;
|
|
const sessionShowTime = visibility.sessionShowTime !== false;
|
|
const weeklyShowLabel = visibility.weeklyShowLabel !== false;
|
|
const weeklyShowBar = visibility.weeklyShowBar !== false;
|
|
const weeklyShowPercentage = visibility.weeklyShowPercentage !== false;
|
|
const weeklyShowCircle = visibility.weeklyShowCircle !== false;
|
|
const weeklyShowTime = visibility.weeklyShowTime !== false;
|
|
|
|
// Session elements
|
|
const sessionLabel = sessionSection.querySelector('.usage-label');
|
|
const sessionBar = sessionSection.querySelector('.progress-bar');
|
|
const sessionPercentage = sessionSection.querySelector('.usage-percentage');
|
|
const sessionTimerContainer = sessionSection.querySelector('.timer-container');
|
|
const sessionTimerSVG = sessionSection.querySelector('.mini-timer');
|
|
const sessionTimeText = sessionSection.querySelector('.timer-text');
|
|
|
|
if (sessionLabel) sessionLabel.style.display = sessionShowLabel ? 'block' : 'none';
|
|
if (sessionBar) sessionBar.style.display = sessionShowBar ? 'block' : 'none';
|
|
if (sessionPercentage) sessionPercentage.style.display = sessionShowPercentage ? 'block' : 'none';
|
|
if (sessionTimerSVG) sessionTimerSVG.style.display = sessionShowCircle ? 'block' : 'none';
|
|
if (sessionTimeText) sessionTimeText.style.display = sessionShowTime ? 'block' : 'none';
|
|
|
|
// Hide timer container if both circle and time are hidden
|
|
if (sessionTimerContainer) {
|
|
sessionTimerContainer.style.display = (sessionShowCircle || sessionShowTime) ? 'flex' : 'none';
|
|
}
|
|
|
|
// Weekly elements
|
|
const weeklyLabel = weeklySection.querySelector('.usage-label');
|
|
const weeklyBar = weeklySection.querySelector('.progress-bar');
|
|
const weeklyPercentage = weeklySection.querySelector('.usage-percentage');
|
|
const weeklyTimerContainer = weeklySection.querySelector('.timer-container');
|
|
const weeklyTimerSVG = weeklySection.querySelector('.mini-timer');
|
|
const weeklyTimeText = weeklySection.querySelector('.timer-text');
|
|
|
|
if (weeklyLabel) weeklyLabel.style.display = weeklyShowLabel ? 'block' : 'none';
|
|
if (weeklyBar) weeklyBar.style.display = weeklyShowBar ? 'block' : 'none';
|
|
if (weeklyPercentage) weeklyPercentage.style.display = weeklyShowPercentage ? 'block' : 'none';
|
|
if (weeklyTimerSVG) weeklyTimerSVG.style.display = weeklyShowCircle ? 'block' : 'none';
|
|
if (weeklyTimeText) weeklyTimeText.style.display = weeklyShowTime ? 'block' : 'none';
|
|
|
|
// Hide timer container if both circle and time are hidden
|
|
if (weeklyTimerContainer) {
|
|
weeklyTimerContainer.style.display = (weeklyShowCircle || weeklyShowTime) ? 'flex' : 'none';
|
|
}
|
|
|
|
// Show/hide entire sections
|
|
if (sessionSection) sessionSection.style.display = showSessionSection ? 'flex' : 'none';
|
|
if (weeklySection) weeklySection.style.display = showWeeklySection ? 'flex' : 'none';
|
|
|
|
// Adjust window height based on visible content
|
|
adjustWindowHeight();
|
|
}
|
|
|
|
// Adjust window size based on visible content
|
|
function adjustWindowHeight() {
|
|
const usageSections = document.querySelectorAll('.usage-section');
|
|
let visibleSections = 0;
|
|
let maxWidthNeeded = 0;
|
|
|
|
usageSections.forEach(section => {
|
|
if (section.style.display !== 'none') {
|
|
visibleSections++;
|
|
|
|
// Calculate width needed for this section
|
|
let sectionWidth = 60; // Base padding (30px left + 30px right for safety)
|
|
|
|
// Check what's visible in this section
|
|
const label = section.querySelector('.usage-label');
|
|
const bar = section.querySelector('.progress-bar');
|
|
const percentage = section.querySelector('.usage-percentage');
|
|
const timerContainer = section.querySelector('.timer-container');
|
|
|
|
// Count visible elements and calculate width
|
|
let visibleElements = 0;
|
|
|
|
if (label && label.style.display !== 'none') {
|
|
sectionWidth += 120; // Label width + buffer
|
|
visibleElements++;
|
|
}
|
|
if (bar && bar.style.display !== 'none') {
|
|
sectionWidth += 150; // Minimum readable bar width
|
|
visibleElements++;
|
|
}
|
|
if (percentage && percentage.style.display !== 'none') {
|
|
sectionWidth += 55; // Percentage width
|
|
visibleElements++;
|
|
}
|
|
if (timerContainer) {
|
|
const timerSVG = timerContainer.querySelector('.mini-timer');
|
|
const timeText = timerContainer.querySelector('.timer-text');
|
|
if ((timerSVG && timerSVG.style.display !== 'none') ||
|
|
(timeText && timeText.style.display !== 'none')) {
|
|
sectionWidth += 100; // Timer container width
|
|
visibleElements++;
|
|
}
|
|
}
|
|
|
|
// Add gaps between elements (16px each)
|
|
if (visibleElements > 1) {
|
|
sectionWidth += (visibleElements - 1) * 16;
|
|
}
|
|
|
|
// Ensure minimum width even if only one element visible
|
|
sectionWidth = Math.max(sectionWidth, 200);
|
|
|
|
maxWidthNeeded = Math.max(maxWidthNeeded, sectionWidth);
|
|
}
|
|
});
|
|
|
|
// Calculate height: title bar (36px) + padding + sections
|
|
const titleBarHeight = 36;
|
|
const contentPadding = 40; // 20px top + 20px bottom
|
|
const sectionHeight = 40;
|
|
const newHeight = titleBarHeight + contentPadding + (visibleSections * sectionHeight);
|
|
|
|
// Title bar needs space for "Claude Usage" + buttons
|
|
// Minimum: logo (19px) + gap (8px) + text (~110px) + buttons (4 * 28px + 3 * 8px gaps) = ~280px
|
|
const minTitleBarWidth = 300;
|
|
|
|
// Ensure we have content width or use minimum
|
|
const contentWidth = Math.max(maxWidthNeeded, 200);
|
|
|
|
// Final dimensions - use the larger of content width or title bar width
|
|
const finalWidth = Math.max(contentWidth, minTitleBarWidth);
|
|
const finalHeight = Math.max(newHeight, titleBarHeight + 60);
|
|
|
|
// Clamp to window constraints
|
|
const clampedWidth = Math.min(Math.max(finalWidth, 320), 600);
|
|
const clampedHeight = Math.min(Math.max(finalHeight, 96), 180);
|
|
|
|
window.electronAPI.setWindowSize({ width: clampedWidth, height: clampedHeight });
|
|
}
|
|
|
|
// Auto-update management
|
|
async function startAutoUpdate() {
|
|
stopAutoUpdate();
|
|
// Load update interval from settings
|
|
const appSettings = await window.electronAPI.getAppSettings();
|
|
const intervalMs = (appSettings.uiUpdateInterval || 300) * 1000; // Convert to milliseconds
|
|
|
|
updateInterval = setInterval(() => {
|
|
fetchUsageData();
|
|
}, intervalMs);
|
|
}
|
|
|
|
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);
|
|
});
|