|
|
@@ -0,0 +1,684 @@
|
|
|
+using System;
|
|
|
+using System.Drawing;
|
|
|
+using System.IO;
|
|
|
+using System.Net.Http;
|
|
|
+using System.Text.Json;
|
|
|
+using System.Threading.Tasks;
|
|
|
+using System.Windows.Forms;
|
|
|
+using System.Text.RegularExpressions;
|
|
|
+using System.Diagnostics;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Text;
|
|
|
+
|
|
|
+namespace qdr.app.tools.claudecodebalancewidget
|
|
|
+{
|
|
|
+ public partial class MainForm : Form
|
|
|
+ {
|
|
|
+ private NotifyIcon notifyIcon;
|
|
|
+ private System.Windows.Forms.Timer refreshTimer;
|
|
|
+ private HttpClient httpClient;
|
|
|
+ private string sessionToken = "";
|
|
|
+ private string apiKey = "";
|
|
|
+ private decimal currentBalance = 0;
|
|
|
+ private bool isVisible = true;
|
|
|
+ private bool isDragging = false;
|
|
|
+ private Point dragStartPoint;
|
|
|
+ private DateTime lastUpdateTime = DateTime.MinValue;
|
|
|
+ private bool isRefreshing = false;
|
|
|
+ private string lastError = "";
|
|
|
+ private AuthenticationMethod authMethod = AuthenticationMethod.SessionCookie;
|
|
|
+
|
|
|
+ public enum AuthenticationMethod
|
|
|
+ {
|
|
|
+ SessionCookie,
|
|
|
+ ApiKey,
|
|
|
+ ConsoleOnly // New method for API key users
|
|
|
+ }
|
|
|
+
|
|
|
+ public MainForm()
|
|
|
+ {
|
|
|
+ InitializeComponent();
|
|
|
+ InitializeSystemTray();
|
|
|
+ InitializeHttpClient();
|
|
|
+ InitializeTimer();
|
|
|
+ SetupTransparentWindow();
|
|
|
+
|
|
|
+ LoadConfiguration();
|
|
|
+ _ = RefreshBalance();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SetupTransparentWindow()
|
|
|
+ {
|
|
|
+ this.BackColor = Color.Lime;
|
|
|
+ this.TransparencyKey = Color.Lime;
|
|
|
+ this.Opacity = 0.95;
|
|
|
+
|
|
|
+ this.SetStyle(ControlStyles.AllPaintingInWmPaint |
|
|
|
+ ControlStyles.UserPaint |
|
|
|
+ ControlStyles.DoubleBuffer |
|
|
|
+ ControlStyles.ResizeRedraw |
|
|
|
+ ControlStyles.SupportsTransparentBackColor, true);
|
|
|
+
|
|
|
+ this.Size = new Size(300, 100);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void InitializeSystemTray()
|
|
|
+ {
|
|
|
+ notifyIcon = new NotifyIcon();
|
|
|
+ // Use the application icon instead of creating a custom one
|
|
|
+ notifyIcon.Icon = this.Icon ?? Icon.ExtractAssociatedIcon(Application.ExecutablePath);
|
|
|
+ notifyIcon.Text = "Claude Balance Monitor";
|
|
|
+ notifyIcon.Visible = true;
|
|
|
+
|
|
|
+ var contextMenu = new ContextMenuStrip();
|
|
|
+
|
|
|
+ var showHideItem = new ToolStripMenuItem("Show/Hide", null, ShowHide_Click);
|
|
|
+ var refreshItem = new ToolStripMenuItem("Refresh Now", null, RefreshNow_Click);
|
|
|
+ var openConsoleItem = new ToolStripMenuItem("Open Console Billing", null, OpenConsole_Click);
|
|
|
+ var settingsItem = new ToolStripMenuItem("Settings...", null, Settings_Click);
|
|
|
+ var aboutItem = new ToolStripMenuItem("About", null, About_Click);
|
|
|
+ var exitItem = new ToolStripMenuItem("Exit", null, Exit_Click);
|
|
|
+
|
|
|
+ contextMenu.Items.Add(showHideItem);
|
|
|
+ contextMenu.Items.Add(refreshItem);
|
|
|
+ contextMenu.Items.Add(openConsoleItem);
|
|
|
+ contextMenu.Items.Add(new ToolStripSeparator());
|
|
|
+ contextMenu.Items.Add(settingsItem);
|
|
|
+ contextMenu.Items.Add(aboutItem);
|
|
|
+ contextMenu.Items.Add(new ToolStripSeparator());
|
|
|
+ contextMenu.Items.Add(exitItem);
|
|
|
+
|
|
|
+ notifyIcon.ContextMenuStrip = contextMenu;
|
|
|
+ notifyIcon.DoubleClick += ShowHide_Click;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Removed CreateIcon method since we're using the application icon now
|
|
|
+
|
|
|
+ private void InitializeHttpClient()
|
|
|
+ {
|
|
|
+ httpClient = new HttpClient();
|
|
|
+ httpClient.Timeout = TimeSpan.FromSeconds(15);
|
|
|
+
|
|
|
+ httpClient.DefaultRequestHeaders.Add("User-Agent",
|
|
|
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
|
|
+ httpClient.DefaultRequestHeaders.Add("Accept",
|
|
|
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
|
|
|
+ httpClient.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.5");
|
|
|
+ httpClient.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
|
|
|
+ httpClient.DefaultRequestHeaders.Add("DNT", "1");
|
|
|
+ httpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");
|
|
|
+ httpClient.DefaultRequestHeaders.Add("Upgrade-Insecure-Requests", "1");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void InitializeTimer()
|
|
|
+ {
|
|
|
+ refreshTimer = new System.Windows.Forms.Timer();
|
|
|
+ refreshTimer.Interval = 300000; // 5 minutes - slower since we're using Console scraping
|
|
|
+ refreshTimer.Tick += async (s, e) => await RefreshBalance();
|
|
|
+ refreshTimer.Start();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void LoadConfiguration()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ string configPath = GetConfigPath();
|
|
|
+ string configDir = Path.GetDirectoryName(configPath);
|
|
|
+
|
|
|
+ if (!Directory.Exists(configDir))
|
|
|
+ {
|
|
|
+ Directory.CreateDirectory(configDir);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (File.Exists(configPath))
|
|
|
+ {
|
|
|
+ var lines = File.ReadAllLines(configPath);
|
|
|
+ foreach (var line in lines)
|
|
|
+ {
|
|
|
+ if (line.StartsWith("SessionToken="))
|
|
|
+ {
|
|
|
+ sessionToken = line.Substring("SessionToken=".Length).Trim();
|
|
|
+ }
|
|
|
+ else if (line.StartsWith("ApiKey="))
|
|
|
+ {
|
|
|
+ apiKey = line.Substring("ApiKey=".Length).Trim();
|
|
|
+ }
|
|
|
+ else if (line.StartsWith("AuthMethod="))
|
|
|
+ {
|
|
|
+ if (Enum.TryParse<AuthenticationMethod>(line.Substring("AuthMethod=".Length).Trim(), out var method))
|
|
|
+ {
|
|
|
+ authMethod = method;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var settingsForm = new SettingsForm("");
|
|
|
+ if (settingsForm.ShowDialog() == DialogResult.OK)
|
|
|
+ {
|
|
|
+ sessionToken = settingsForm.SessionToken;
|
|
|
+ SaveConfiguration();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ lastError = $"Config error: {ex.Message}";
|
|
|
+ MessageBox.Show($"Error loading configuration: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private string GetConfigPath()
|
|
|
+ {
|
|
|
+ return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
|
+ "ClaudeBalanceMonitor", "config.txt");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SaveConfiguration()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ string configPath = GetConfigPath();
|
|
|
+ Directory.CreateDirectory(Path.GetDirectoryName(configPath));
|
|
|
+
|
|
|
+ var lines = new List<string>
|
|
|
+ {
|
|
|
+ $"SessionToken={sessionToken}",
|
|
|
+ $"ApiKey={apiKey}",
|
|
|
+ $"AuthMethod={authMethod}"
|
|
|
+ };
|
|
|
+
|
|
|
+ File.WriteAllLines(configPath, lines);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ MessageBox.Show($"Error saving configuration: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task RefreshBalance()
|
|
|
+ {
|
|
|
+ if (isRefreshing) return;
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ isRefreshing = true;
|
|
|
+ lastError = "";
|
|
|
+
|
|
|
+ decimal balance = -1;
|
|
|
+
|
|
|
+ switch (authMethod)
|
|
|
+ {
|
|
|
+ case AuthenticationMethod.ApiKey:
|
|
|
+ case AuthenticationMethod.ConsoleOnly:
|
|
|
+ balance = await GetBalanceViaConsoleWithApiKey();
|
|
|
+ break;
|
|
|
+ case AuthenticationMethod.SessionCookie:
|
|
|
+ balance = await GetBalanceViaSessionCookie();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ UpdateBalance(balance, balance >= 0 ? "" : lastError);
|
|
|
+ if (balance >= 0)
|
|
|
+ {
|
|
|
+ lastUpdateTime = DateTime.Now;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ UpdateBalance(-1, ex.Message);
|
|
|
+ Console.WriteLine($"Error refreshing balance: {ex.Message}");
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ isRefreshing = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<decimal> GetBalanceViaConsoleWithApiKey()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ if (string.IsNullOrEmpty(apiKey))
|
|
|
+ {
|
|
|
+ lastError = "No API key configured";
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Since there's no direct API endpoint for balance, we need to:
|
|
|
+ // 1. First verify the API key works
|
|
|
+ // 2. Then try to access the Console billing page
|
|
|
+
|
|
|
+ // Step 1: Verify API key with a minimal request
|
|
|
+ var isValidApiKey = await VerifyApiKey();
|
|
|
+ if (!isValidApiKey)
|
|
|
+ {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Step 2: Try to access Console billing page
|
|
|
+ // This is tricky because we need to be logged in to Console
|
|
|
+ // For now, we'll show a message that the user needs to check manually
|
|
|
+ lastError = "API key valid - Check Console manually for balance";
|
|
|
+ return -2; // Special code to indicate "check manually"
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ lastError = $"API Error: {ex.Message}";
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<bool> VerifyApiKey()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // Make a small test request to verify the API key works
|
|
|
+ var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages");
|
|
|
+ request.Headers.Add("x-api-key", apiKey);
|
|
|
+ request.Headers.Add("anthropic-version", "2023-06-01");
|
|
|
+
|
|
|
+ var testContent = @"{
|
|
|
+ ""model"": ""claude-3-5-haiku-20241022"",
|
|
|
+ ""max_tokens"": 1,
|
|
|
+ ""messages"": [{
|
|
|
+ ""role"": ""user"",
|
|
|
+ ""content"": ""Hi""
|
|
|
+ }]
|
|
|
+ }";
|
|
|
+
|
|
|
+ request.Content = new StringContent(testContent, Encoding.UTF8, "application/json");
|
|
|
+
|
|
|
+ var response = await httpClient.SendAsync(request);
|
|
|
+
|
|
|
+ if (response.IsSuccessStatusCode)
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
|
|
+ {
|
|
|
+ lastError = "Invalid API key";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ else if (response.StatusCode == System.Net.HttpStatusCode.PaymentRequired)
|
|
|
+ {
|
|
|
+ lastError = "No credits available";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ lastError = $"API verification failed: {response.StatusCode}";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ lastError = $"API verification error: {ex.Message}";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<decimal> GetBalanceViaSessionCookie()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ if (string.IsNullOrEmpty(sessionToken))
|
|
|
+ {
|
|
|
+ lastError = "No session token configured";
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Clear existing headers
|
|
|
+ httpClient.DefaultRequestHeaders.Remove("Cookie");
|
|
|
+ httpClient.DefaultRequestHeaders.Remove("X-Requested-With");
|
|
|
+ httpClient.DefaultRequestHeaders.Remove("Referer");
|
|
|
+
|
|
|
+ // Add session authentication with enhanced headers
|
|
|
+ httpClient.DefaultRequestHeaders.Add("Cookie", $"sessionKey={sessionToken}");
|
|
|
+ httpClient.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest");
|
|
|
+ httpClient.DefaultRequestHeaders.Add("Referer", "https://console.anthropic.com/");
|
|
|
+ httpClient.DefaultRequestHeaders.Add("Sec-Fetch-Dest", "document");
|
|
|
+ httpClient.DefaultRequestHeaders.Add("Sec-Fetch-Mode", "navigate");
|
|
|
+ httpClient.DefaultRequestHeaders.Add("Sec-Fetch-Site", "same-origin");
|
|
|
+
|
|
|
+ var response = await httpClient.GetAsync("https://console.anthropic.com/settings/billing");
|
|
|
+
|
|
|
+ if (response.IsSuccessStatusCode)
|
|
|
+ {
|
|
|
+ var html = await response.Content.ReadAsStringAsync();
|
|
|
+ return ExtractBalanceFromHtml(html);
|
|
|
+ }
|
|
|
+ else if (response.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
|
|
+ {
|
|
|
+ lastError = "Forbidden - Session token invalid or requires additional auth";
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ lastError = $"HTTP {response.StatusCode}";
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ lastError = $"Session Error: {ex.Message}";
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private decimal ExtractBalanceFromHtml(string html)
|
|
|
+ {
|
|
|
+ // Enhanced patterns for finding credit balance
|
|
|
+ var patterns = new[]
|
|
|
+ {
|
|
|
+ @"Credit balance[\s\S]*?\$?([\d,]+\.?\d*)",
|
|
|
+ @"Available credit[\s\S]*?\$?([\d,]+\.?\d*)",
|
|
|
+ @"remaining[^>]*>[\s]*\$?([\d,]+\.?\d*)",
|
|
|
+ @"balance[^>]*>[\s]*\$?([\d,]+\.?\d*)",
|
|
|
+ @"credit[^>]*>[\s]*\$?([\d,]+\.?\d*)",
|
|
|
+ @"\$?([\d,]+\.?\d*)[^<]*remaining",
|
|
|
+ @"""credit_balance"":\s*""?\$?([\d,]+\.?\d*)""?",
|
|
|
+ @"""balance"":\s*""?\$?([\d,]+\.?\d*)""?",
|
|
|
+ @"data-balance=""?\$?([\d,]+\.?\d*)""?",
|
|
|
+ @"balance.*?(\d+\.?\d*)",
|
|
|
+ @"Credits?[\s\S]*?\$?([\d,]+\.?\d*)",
|
|
|
+ @"\$?([\d,]+\.?\d*)[\s]*credit"
|
|
|
+ };
|
|
|
+
|
|
|
+ foreach (var pattern in patterns)
|
|
|
+ {
|
|
|
+ var matches = Regex.Matches(html, pattern, RegexOptions.IgnoreCase);
|
|
|
+ foreach (Match match in matches)
|
|
|
+ {
|
|
|
+ var balanceStr = match.Groups[1].Value.Replace(",", "");
|
|
|
+ if (decimal.TryParse(balanceStr, out decimal balance) && balance >= 0)
|
|
|
+ {
|
|
|
+ return balance;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ lastError = "Could not find balance in Console page";
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateBalance(decimal balance, string error = "")
|
|
|
+ {
|
|
|
+ if (InvokeRequired)
|
|
|
+ {
|
|
|
+ Invoke(new Action<decimal, string>(UpdateBalance), balance, error);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ currentBalance = balance;
|
|
|
+ lastError = error;
|
|
|
+
|
|
|
+ if (balance >= 0)
|
|
|
+ {
|
|
|
+ notifyIcon.Text = $"Claude Balance: ${balance:F2}\nLast updated: {DateTime.Now:HH:mm:ss}\nMethod: {authMethod}";
|
|
|
+ }
|
|
|
+ else if (balance == -2) // Special case for "check manually"
|
|
|
+ {
|
|
|
+ notifyIcon.Text = $"Claude API Key Valid\nCheck Console for balance\nRight-click → Open Console";
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ notifyIcon.Text = $"Claude Balance: Error\n{(string.IsNullOrEmpty(error) ? "Unknown error" : error)}\nMethod: {authMethod}";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isVisible)
|
|
|
+ {
|
|
|
+ Invalidate();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void MainForm_Paint(object sender, PaintEventArgs e)
|
|
|
+ {
|
|
|
+ var g = e.Graphics;
|
|
|
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
|
|
+ g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
|
|
|
+
|
|
|
+ // Draw background with rounded corners
|
|
|
+ var rect = new Rectangle(5, 5, Width - 10, Height - 10);
|
|
|
+ using (var path = CreateRoundedRectangle(rect, 12))
|
|
|
+ {
|
|
|
+ using (var brush = new System.Drawing.Drawing2D.LinearGradientBrush(rect,
|
|
|
+ Color.FromArgb(200, 20, 25, 35),
|
|
|
+ Color.FromArgb(180, 40, 45, 60),
|
|
|
+ System.Drawing.Drawing2D.LinearGradientMode.Vertical))
|
|
|
+ {
|
|
|
+ g.FillPath(brush, path);
|
|
|
+ }
|
|
|
+
|
|
|
+ using (var pen = new Pen(Color.FromArgb(100, 255, 255, 255), 1))
|
|
|
+ {
|
|
|
+ g.DrawPath(pen, path);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Draw balance text
|
|
|
+ string balanceText;
|
|
|
+ Color textColor;
|
|
|
+
|
|
|
+ if (currentBalance >= 0)
|
|
|
+ {
|
|
|
+ balanceText = $"${currentBalance:F2}";
|
|
|
+ textColor = currentBalance > 5 ? Color.FromArgb(255, 144, 238, 144) :
|
|
|
+ currentBalance > 1 ? Color.FromArgb(255, 255, 215, 0) :
|
|
|
+ Color.FromArgb(255, 255, 140, 140);
|
|
|
+ }
|
|
|
+ else if (currentBalance == -2) // Special case for "check manually"
|
|
|
+ {
|
|
|
+ balanceText = "Check Console";
|
|
|
+ textColor = Color.FromArgb(255, 100, 149, 237);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ balanceText = "Error";
|
|
|
+ textColor = Color.FromArgb(255, 255, 100, 100);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Main balance text
|
|
|
+ using (var font = new Font("Segoe UI", currentBalance == -2 ? 10 : 13, FontStyle.Bold))
|
|
|
+ using (var brush = new SolidBrush(textColor))
|
|
|
+ {
|
|
|
+ var textSize = g.MeasureString(balanceText, font);
|
|
|
+ var x = (Width - textSize.Width) / 2;
|
|
|
+ var y = currentBalance == -2 ? 18 : 12;
|
|
|
+ g.DrawString(balanceText, font, brush, x, y);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Status text
|
|
|
+ string statusText;
|
|
|
+ if (currentBalance == -2)
|
|
|
+ {
|
|
|
+ statusText = "API Key Valid - Click to open Console";
|
|
|
+ }
|
|
|
+ else if (!string.IsNullOrEmpty(lastError))
|
|
|
+ {
|
|
|
+ statusText = lastError.Length > 30 ? lastError.Substring(0, 30) + "..." : lastError;
|
|
|
+ }
|
|
|
+ else if (lastUpdateTime != DateTime.MinValue)
|
|
|
+ {
|
|
|
+ statusText = $"Updated: {lastUpdateTime:HH:mm:ss} ({authMethod})";
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ statusText = "Loading...";
|
|
|
+ }
|
|
|
+
|
|
|
+ using (var font = new Font("Segoe UI", 7.5f))
|
|
|
+ using (var brush = new SolidBrush(Color.FromArgb(180, 200, 200, 200)))
|
|
|
+ {
|
|
|
+ var textSize = g.MeasureString(statusText, font);
|
|
|
+ var x = (Width - textSize.Width) / 2;
|
|
|
+ var y = Height - 22;
|
|
|
+ g.DrawString(statusText, font, brush, x, y);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private System.Drawing.Drawing2D.GraphicsPath CreateRoundedRectangle(Rectangle rect, int radius)
|
|
|
+ {
|
|
|
+ var path = new System.Drawing.Drawing2D.GraphicsPath();
|
|
|
+ var diameter = radius * 2;
|
|
|
+
|
|
|
+ path.AddArc(rect.X, rect.Y, diameter, diameter, 180, 90);
|
|
|
+ path.AddArc(rect.Right - diameter, rect.Y, diameter, diameter, 270, 90);
|
|
|
+ path.AddArc(rect.Right - diameter, rect.Bottom - diameter, diameter, diameter, 0, 90);
|
|
|
+ path.AddArc(rect.X, rect.Bottom - diameter, diameter, diameter, 90, 90);
|
|
|
+ path.CloseFigure();
|
|
|
+
|
|
|
+ return path;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async void RefreshNow_Click(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ await RefreshBalance();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OpenConsole_Click(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ Process.Start(new ProcessStartInfo
|
|
|
+ {
|
|
|
+ FileName = "https://console.anthropic.com/settings/billing",
|
|
|
+ UseShellExecute = true
|
|
|
+ });
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ MessageBox.Show($"Could not open browser: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void About_Click(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ MessageBox.Show(
|
|
|
+ $"Claude Code Balance Widget v{System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}\n\n" +
|
|
|
+ "⚠️ IMPORTANT: Anthropic doesn't provide a balance API endpoint.\n" +
|
|
|
+ "This widget can verify your API key but cannot automatically\n" +
|
|
|
+ "retrieve your balance. You'll need to check the Console manually.\n\n" +
|
|
|
+ "Created by Dalibor Votruba (Quadarax)\n\n" +
|
|
|
+ "Supported Methods:\n" +
|
|
|
+ "• API Key Verification + Manual Console Check\n" +
|
|
|
+ "• Session Cookie (may not work due to security)",
|
|
|
+ "About Claude Balance Monitor",
|
|
|
+ MessageBoxButtons.OK,
|
|
|
+ MessageBoxIcon.Information
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ShowHide_Click(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ if (isVisible)
|
|
|
+ {
|
|
|
+ Hide();
|
|
|
+ isVisible = false;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Show();
|
|
|
+ WindowState = FormWindowState.Normal;
|
|
|
+ isVisible = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Settings_Click(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ var settingsForm = new SettingsForm(sessionToken);
|
|
|
+ if (settingsForm.ShowDialog() == DialogResult.OK)
|
|
|
+ {
|
|
|
+ sessionToken = settingsForm.SessionToken;
|
|
|
+ SaveConfiguration();
|
|
|
+ _ = RefreshBalance();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Exit_Click(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ refreshTimer?.Stop();
|
|
|
+ notifyIcon?.Dispose();
|
|
|
+ httpClient?.Dispose();
|
|
|
+ Application.Exit();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Mouse event handlers for dragging and clicking
|
|
|
+ private void MainForm_MouseDown(object sender, MouseEventArgs e)
|
|
|
+ {
|
|
|
+ if (e.Button == MouseButtons.Left)
|
|
|
+ {
|
|
|
+ isDragging = true;
|
|
|
+ dragStartPoint = e.Location;
|
|
|
+ this.Cursor = Cursors.SizeAll;
|
|
|
+ }
|
|
|
+ else if (e.Button == MouseButtons.Right)
|
|
|
+ {
|
|
|
+ notifyIcon.ContextMenuStrip.Show(Cursor.Position);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void MainForm_MouseMove(object sender, MouseEventArgs e)
|
|
|
+ {
|
|
|
+ if (isDragging)
|
|
|
+ {
|
|
|
+ Point screenPoint = PointToScreen(e.Location);
|
|
|
+ Location = new Point(screenPoint.X - dragStartPoint.X, screenPoint.Y - dragStartPoint.Y);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ this.Cursor = currentBalance == -2 ? Cursors.Hand : Cursors.SizeAll;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void MainForm_MouseUp(object sender, MouseEventArgs e)
|
|
|
+ {
|
|
|
+ if (e.Button == MouseButtons.Left)
|
|
|
+ {
|
|
|
+ if (isDragging)
|
|
|
+ {
|
|
|
+ // Check if this was actually a drag or just a click
|
|
|
+ var dragDistance = Math.Sqrt(Math.Pow(e.X - dragStartPoint.X, 2) + Math.Pow(e.Y - dragStartPoint.Y, 2));
|
|
|
+
|
|
|
+ if (dragDistance < 5) // Less than 5 pixels = click, not drag
|
|
|
+ {
|
|
|
+ // This was a click, not a drag
|
|
|
+ if (currentBalance == -2) // If showing "Check Console", open console on click
|
|
|
+ {
|
|
|
+ OpenConsole_Click(sender, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ isDragging = false;
|
|
|
+ this.Cursor = currentBalance == -2 ? Cursors.Hand : Cursors.Default;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected override void OnFormClosing(FormClosingEventArgs e)
|
|
|
+ {
|
|
|
+ if (e.CloseReason == CloseReason.UserClosing)
|
|
|
+ {
|
|
|
+ e.Cancel = true;
|
|
|
+ Hide();
|
|
|
+ isVisible = false;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ base.OnFormClosing(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected override void Dispose(bool disposing)
|
|
|
+ {
|
|
|
+ if (disposing)
|
|
|
+ {
|
|
|
+ refreshTimer?.Dispose();
|
|
|
+ notifyIcon?.Dispose();
|
|
|
+ httpClient?.Dispose();
|
|
|
+ }
|
|
|
+ base.Dispose(disposing);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|