MainForm.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. using System;
  2. using System.Drawing;
  3. using System.IO;
  4. using System.Net.Http;
  5. using System.Text.Json;
  6. using System.Threading.Tasks;
  7. using System.Windows.Forms;
  8. using System.Text.RegularExpressions;
  9. using System.Diagnostics;
  10. using System.Collections.Generic;
  11. using System.Text;
  12. namespace qdr.app.tools.claudecodebalancewidget
  13. {
  14. public partial class MainForm : Form
  15. {
  16. private NotifyIcon notifyIcon;
  17. private System.Windows.Forms.Timer refreshTimer;
  18. private HttpClient httpClient;
  19. private string sessionToken = "";
  20. private string apiKey = "";
  21. private decimal currentBalance = 0;
  22. private bool isVisible = true;
  23. private bool isDragging = false;
  24. private Point dragStartPoint;
  25. private DateTime lastUpdateTime = DateTime.MinValue;
  26. private bool isRefreshing = false;
  27. private string lastError = "";
  28. private AuthenticationMethod authMethod = AuthenticationMethod.SessionCookie;
  29. public enum AuthenticationMethod
  30. {
  31. SessionCookie,
  32. ApiKey,
  33. ConsoleOnly // New method for API key users
  34. }
  35. public MainForm()
  36. {
  37. InitializeComponent();
  38. InitializeSystemTray();
  39. InitializeHttpClient();
  40. InitializeTimer();
  41. SetupTransparentWindow();
  42. LoadConfiguration();
  43. _ = RefreshBalance();
  44. }
  45. private void SetupTransparentWindow()
  46. {
  47. this.BackColor = Color.Lime;
  48. this.TransparencyKey = Color.Lime;
  49. this.Opacity = 0.95;
  50. this.SetStyle(ControlStyles.AllPaintingInWmPaint |
  51. ControlStyles.UserPaint |
  52. ControlStyles.DoubleBuffer |
  53. ControlStyles.ResizeRedraw |
  54. ControlStyles.SupportsTransparentBackColor, true);
  55. this.Size = new Size(300, 100);
  56. }
  57. private void InitializeSystemTray()
  58. {
  59. notifyIcon = new NotifyIcon();
  60. // Use the application icon instead of creating a custom one
  61. notifyIcon.Icon = this.Icon ?? Icon.ExtractAssociatedIcon(Application.ExecutablePath);
  62. notifyIcon.Text = "Claude Balance Monitor";
  63. notifyIcon.Visible = true;
  64. var contextMenu = new ContextMenuStrip();
  65. var showHideItem = new ToolStripMenuItem("Show/Hide", null, ShowHide_Click);
  66. var refreshItem = new ToolStripMenuItem("Refresh Now", null, RefreshNow_Click);
  67. var openConsoleItem = new ToolStripMenuItem("Open Console Billing", null, OpenConsole_Click);
  68. var settingsItem = new ToolStripMenuItem("Settings...", null, Settings_Click);
  69. var aboutItem = new ToolStripMenuItem("About", null, About_Click);
  70. var exitItem = new ToolStripMenuItem("Exit", null, Exit_Click);
  71. contextMenu.Items.Add(showHideItem);
  72. contextMenu.Items.Add(refreshItem);
  73. contextMenu.Items.Add(openConsoleItem);
  74. contextMenu.Items.Add(new ToolStripSeparator());
  75. contextMenu.Items.Add(settingsItem);
  76. contextMenu.Items.Add(aboutItem);
  77. contextMenu.Items.Add(new ToolStripSeparator());
  78. contextMenu.Items.Add(exitItem);
  79. notifyIcon.ContextMenuStrip = contextMenu;
  80. notifyIcon.DoubleClick += ShowHide_Click;
  81. }
  82. // Removed CreateIcon method since we're using the application icon now
  83. private void InitializeHttpClient()
  84. {
  85. httpClient = new HttpClient();
  86. httpClient.Timeout = TimeSpan.FromSeconds(15);
  87. httpClient.DefaultRequestHeaders.Add("User-Agent",
  88. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
  89. httpClient.DefaultRequestHeaders.Add("Accept",
  90. "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
  91. httpClient.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.5");
  92. httpClient.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
  93. httpClient.DefaultRequestHeaders.Add("DNT", "1");
  94. httpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");
  95. httpClient.DefaultRequestHeaders.Add("Upgrade-Insecure-Requests", "1");
  96. }
  97. private void InitializeTimer()
  98. {
  99. refreshTimer = new System.Windows.Forms.Timer();
  100. refreshTimer.Interval = 300000; // 5 minutes - slower since we're using Console scraping
  101. refreshTimer.Tick += async (s, e) => await RefreshBalance();
  102. refreshTimer.Start();
  103. }
  104. private void LoadConfiguration()
  105. {
  106. try
  107. {
  108. string configPath = GetConfigPath();
  109. string configDir = Path.GetDirectoryName(configPath);
  110. if (!Directory.Exists(configDir))
  111. {
  112. Directory.CreateDirectory(configDir);
  113. }
  114. if (File.Exists(configPath))
  115. {
  116. var lines = File.ReadAllLines(configPath);
  117. foreach (var line in lines)
  118. {
  119. if (line.StartsWith("SessionToken="))
  120. {
  121. sessionToken = line.Substring("SessionToken=".Length).Trim();
  122. }
  123. else if (line.StartsWith("ApiKey="))
  124. {
  125. apiKey = line.Substring("ApiKey=".Length).Trim();
  126. }
  127. else if (line.StartsWith("AuthMethod="))
  128. {
  129. if (Enum.TryParse<AuthenticationMethod>(line.Substring("AuthMethod=".Length).Trim(), out var method))
  130. {
  131. authMethod = method;
  132. }
  133. }
  134. }
  135. }
  136. else
  137. {
  138. var settingsForm = new SettingsForm("");
  139. if (settingsForm.ShowDialog() == DialogResult.OK)
  140. {
  141. sessionToken = settingsForm.SessionToken;
  142. SaveConfiguration();
  143. }
  144. }
  145. }
  146. catch (Exception ex)
  147. {
  148. lastError = $"Config error: {ex.Message}";
  149. MessageBox.Show($"Error loading configuration: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
  150. }
  151. }
  152. private string GetConfigPath()
  153. {
  154. return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
  155. "ClaudeBalanceMonitor", "config.txt");
  156. }
  157. private void SaveConfiguration()
  158. {
  159. try
  160. {
  161. string configPath = GetConfigPath();
  162. Directory.CreateDirectory(Path.GetDirectoryName(configPath));
  163. var lines = new List<string>
  164. {
  165. $"SessionToken={sessionToken}",
  166. $"ApiKey={apiKey}",
  167. $"AuthMethod={authMethod}"
  168. };
  169. File.WriteAllLines(configPath, lines);
  170. }
  171. catch (Exception ex)
  172. {
  173. MessageBox.Show($"Error saving configuration: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
  174. }
  175. }
  176. private async Task RefreshBalance()
  177. {
  178. if (isRefreshing) return;
  179. try
  180. {
  181. isRefreshing = true;
  182. lastError = "";
  183. decimal balance = -1;
  184. switch (authMethod)
  185. {
  186. case AuthenticationMethod.ApiKey:
  187. case AuthenticationMethod.ConsoleOnly:
  188. balance = await GetBalanceViaConsoleWithApiKey();
  189. break;
  190. case AuthenticationMethod.SessionCookie:
  191. balance = await GetBalanceViaSessionCookie();
  192. break;
  193. }
  194. UpdateBalance(balance, balance >= 0 ? "" : lastError);
  195. if (balance >= 0)
  196. {
  197. lastUpdateTime = DateTime.Now;
  198. }
  199. }
  200. catch (Exception ex)
  201. {
  202. UpdateBalance(-1, ex.Message);
  203. Console.WriteLine($"Error refreshing balance: {ex.Message}");
  204. }
  205. finally
  206. {
  207. isRefreshing = false;
  208. }
  209. }
  210. private async Task<decimal> GetBalanceViaConsoleWithApiKey()
  211. {
  212. try
  213. {
  214. if (string.IsNullOrEmpty(apiKey))
  215. {
  216. lastError = "No API key configured";
  217. return -1;
  218. }
  219. // Since there's no direct API endpoint for balance, we need to:
  220. // 1. First verify the API key works
  221. // 2. Then try to access the Console billing page
  222. // Step 1: Verify API key with a minimal request
  223. var isValidApiKey = await VerifyApiKey();
  224. if (!isValidApiKey)
  225. {
  226. return -1;
  227. }
  228. // Step 2: Try to access Console billing page
  229. // This is tricky because we need to be logged in to Console
  230. // For now, we'll show a message that the user needs to check manually
  231. lastError = "API key valid - Check Console manually for balance";
  232. return -2; // Special code to indicate "check manually"
  233. }
  234. catch (Exception ex)
  235. {
  236. lastError = $"API Error: {ex.Message}";
  237. return -1;
  238. }
  239. }
  240. private async Task<bool> VerifyApiKey()
  241. {
  242. try
  243. {
  244. // Make a small test request to verify the API key works
  245. var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages");
  246. request.Headers.Add("x-api-key", apiKey);
  247. request.Headers.Add("anthropic-version", "2023-06-01");
  248. var testContent = @"{
  249. ""model"": ""claude-3-5-haiku-20241022"",
  250. ""max_tokens"": 1,
  251. ""messages"": [{
  252. ""role"": ""user"",
  253. ""content"": ""Hi""
  254. }]
  255. }";
  256. request.Content = new StringContent(testContent, Encoding.UTF8, "application/json");
  257. var response = await httpClient.SendAsync(request);
  258. if (response.IsSuccessStatusCode)
  259. {
  260. return true;
  261. }
  262. else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
  263. {
  264. lastError = "Invalid API key";
  265. return false;
  266. }
  267. else if (response.StatusCode == System.Net.HttpStatusCode.PaymentRequired)
  268. {
  269. lastError = "No credits available";
  270. return false;
  271. }
  272. else
  273. {
  274. lastError = $"API verification failed: {response.StatusCode}";
  275. return false;
  276. }
  277. }
  278. catch (Exception ex)
  279. {
  280. lastError = $"API verification error: {ex.Message}";
  281. return false;
  282. }
  283. }
  284. private async Task<decimal> GetBalanceViaSessionCookie()
  285. {
  286. try
  287. {
  288. if (string.IsNullOrEmpty(sessionToken))
  289. {
  290. lastError = "No session token configured";
  291. return -1;
  292. }
  293. // Clear existing headers
  294. httpClient.DefaultRequestHeaders.Remove("Cookie");
  295. httpClient.DefaultRequestHeaders.Remove("X-Requested-With");
  296. httpClient.DefaultRequestHeaders.Remove("Referer");
  297. // Add session authentication with enhanced headers
  298. httpClient.DefaultRequestHeaders.Add("Cookie", $"sessionKey={sessionToken}");
  299. httpClient.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest");
  300. httpClient.DefaultRequestHeaders.Add("Referer", "https://console.anthropic.com/");
  301. httpClient.DefaultRequestHeaders.Add("Sec-Fetch-Dest", "document");
  302. httpClient.DefaultRequestHeaders.Add("Sec-Fetch-Mode", "navigate");
  303. httpClient.DefaultRequestHeaders.Add("Sec-Fetch-Site", "same-origin");
  304. var response = await httpClient.GetAsync("https://console.anthropic.com/settings/billing");
  305. if (response.IsSuccessStatusCode)
  306. {
  307. var html = await response.Content.ReadAsStringAsync();
  308. return ExtractBalanceFromHtml(html);
  309. }
  310. else if (response.StatusCode == System.Net.HttpStatusCode.Forbidden)
  311. {
  312. lastError = "Forbidden - Session token invalid or requires additional auth";
  313. return -1;
  314. }
  315. else
  316. {
  317. lastError = $"HTTP {response.StatusCode}";
  318. return -1;
  319. }
  320. }
  321. catch (Exception ex)
  322. {
  323. lastError = $"Session Error: {ex.Message}";
  324. return -1;
  325. }
  326. }
  327. private decimal ExtractBalanceFromHtml(string html)
  328. {
  329. // Enhanced patterns for finding credit balance
  330. var patterns = new[]
  331. {
  332. @"Credit balance[\s\S]*?\$?([\d,]+\.?\d*)",
  333. @"Available credit[\s\S]*?\$?([\d,]+\.?\d*)",
  334. @"remaining[^>]*>[\s]*\$?([\d,]+\.?\d*)",
  335. @"balance[^>]*>[\s]*\$?([\d,]+\.?\d*)",
  336. @"credit[^>]*>[\s]*\$?([\d,]+\.?\d*)",
  337. @"\$?([\d,]+\.?\d*)[^<]*remaining",
  338. @"""credit_balance"":\s*""?\$?([\d,]+\.?\d*)""?",
  339. @"""balance"":\s*""?\$?([\d,]+\.?\d*)""?",
  340. @"data-balance=""?\$?([\d,]+\.?\d*)""?",
  341. @"balance.*?(\d+\.?\d*)",
  342. @"Credits?[\s\S]*?\$?([\d,]+\.?\d*)",
  343. @"\$?([\d,]+\.?\d*)[\s]*credit"
  344. };
  345. foreach (var pattern in patterns)
  346. {
  347. var matches = Regex.Matches(html, pattern, RegexOptions.IgnoreCase);
  348. foreach (Match match in matches)
  349. {
  350. var balanceStr = match.Groups[1].Value.Replace(",", "");
  351. if (decimal.TryParse(balanceStr, out decimal balance) && balance >= 0)
  352. {
  353. return balance;
  354. }
  355. }
  356. }
  357. lastError = "Could not find balance in Console page";
  358. return -1;
  359. }
  360. private void UpdateBalance(decimal balance, string error = "")
  361. {
  362. if (InvokeRequired)
  363. {
  364. Invoke(new Action<decimal, string>(UpdateBalance), balance, error);
  365. return;
  366. }
  367. currentBalance = balance;
  368. lastError = error;
  369. if (balance >= 0)
  370. {
  371. notifyIcon.Text = $"Claude Balance: ${balance:F2}\nLast updated: {DateTime.Now:HH:mm:ss}\nMethod: {authMethod}";
  372. }
  373. else if (balance == -2) // Special case for "check manually"
  374. {
  375. notifyIcon.Text = $"Claude API Key Valid\nCheck Console for balance\nRight-click → Open Console";
  376. }
  377. else
  378. {
  379. notifyIcon.Text = $"Claude Balance: Error\n{(string.IsNullOrEmpty(error) ? "Unknown error" : error)}\nMethod: {authMethod}";
  380. }
  381. if (isVisible)
  382. {
  383. Invalidate();
  384. }
  385. }
  386. private void MainForm_Paint(object sender, PaintEventArgs e)
  387. {
  388. var g = e.Graphics;
  389. g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
  390. g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
  391. // Draw background with rounded corners
  392. var rect = new Rectangle(5, 5, Width - 10, Height - 10);
  393. using (var path = CreateRoundedRectangle(rect, 12))
  394. {
  395. using (var brush = new System.Drawing.Drawing2D.LinearGradientBrush(rect,
  396. Color.FromArgb(200, 20, 25, 35),
  397. Color.FromArgb(180, 40, 45, 60),
  398. System.Drawing.Drawing2D.LinearGradientMode.Vertical))
  399. {
  400. g.FillPath(brush, path);
  401. }
  402. using (var pen = new Pen(Color.FromArgb(100, 255, 255, 255), 1))
  403. {
  404. g.DrawPath(pen, path);
  405. }
  406. }
  407. // Draw balance text
  408. string balanceText;
  409. Color textColor;
  410. if (currentBalance >= 0)
  411. {
  412. balanceText = $"${currentBalance:F2}";
  413. textColor = currentBalance > 5 ? Color.FromArgb(255, 144, 238, 144) :
  414. currentBalance > 1 ? Color.FromArgb(255, 255, 215, 0) :
  415. Color.FromArgb(255, 255, 140, 140);
  416. }
  417. else if (currentBalance == -2) // Special case for "check manually"
  418. {
  419. balanceText = "Check Console";
  420. textColor = Color.FromArgb(255, 100, 149, 237);
  421. }
  422. else
  423. {
  424. balanceText = "Error";
  425. textColor = Color.FromArgb(255, 255, 100, 100);
  426. }
  427. // Main balance text
  428. using (var font = new Font("Segoe UI", currentBalance == -2 ? 10 : 13, FontStyle.Bold))
  429. using (var brush = new SolidBrush(textColor))
  430. {
  431. var textSize = g.MeasureString(balanceText, font);
  432. var x = (Width - textSize.Width) / 2;
  433. var y = currentBalance == -2 ? 18 : 12;
  434. g.DrawString(balanceText, font, brush, x, y);
  435. }
  436. // Status text
  437. string statusText;
  438. if (currentBalance == -2)
  439. {
  440. statusText = "API Key Valid - Click to open Console";
  441. }
  442. else if (!string.IsNullOrEmpty(lastError))
  443. {
  444. statusText = lastError.Length > 30 ? lastError.Substring(0, 30) + "..." : lastError;
  445. }
  446. else if (lastUpdateTime != DateTime.MinValue)
  447. {
  448. statusText = $"Updated: {lastUpdateTime:HH:mm:ss} ({authMethod})";
  449. }
  450. else
  451. {
  452. statusText = "Loading...";
  453. }
  454. using (var font = new Font("Segoe UI", 7.5f))
  455. using (var brush = new SolidBrush(Color.FromArgb(180, 200, 200, 200)))
  456. {
  457. var textSize = g.MeasureString(statusText, font);
  458. var x = (Width - textSize.Width) / 2;
  459. var y = Height - 22;
  460. g.DrawString(statusText, font, brush, x, y);
  461. }
  462. }
  463. private System.Drawing.Drawing2D.GraphicsPath CreateRoundedRectangle(Rectangle rect, int radius)
  464. {
  465. var path = new System.Drawing.Drawing2D.GraphicsPath();
  466. var diameter = radius * 2;
  467. path.AddArc(rect.X, rect.Y, diameter, diameter, 180, 90);
  468. path.AddArc(rect.Right - diameter, rect.Y, diameter, diameter, 270, 90);
  469. path.AddArc(rect.Right - diameter, rect.Bottom - diameter, diameter, diameter, 0, 90);
  470. path.AddArc(rect.X, rect.Bottom - diameter, diameter, diameter, 90, 90);
  471. path.CloseFigure();
  472. return path;
  473. }
  474. private async void RefreshNow_Click(object sender, EventArgs e)
  475. {
  476. await RefreshBalance();
  477. }
  478. private void OpenConsole_Click(object sender, EventArgs e)
  479. {
  480. try
  481. {
  482. Process.Start(new ProcessStartInfo
  483. {
  484. FileName = "https://console.anthropic.com/settings/billing",
  485. UseShellExecute = true
  486. });
  487. }
  488. catch (Exception ex)
  489. {
  490. MessageBox.Show($"Could not open browser: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
  491. }
  492. }
  493. private void About_Click(object sender, EventArgs e)
  494. {
  495. MessageBox.Show(
  496. $"Claude Code Balance Widget v{System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}\n\n" +
  497. "⚠️ IMPORTANT: Anthropic doesn't provide a balance API endpoint.\n" +
  498. "This widget can verify your API key but cannot automatically\n" +
  499. "retrieve your balance. You'll need to check the Console manually.\n\n" +
  500. "Created by Dalibor Votruba (Quadarax)\n\n" +
  501. "Supported Methods:\n" +
  502. "• API Key Verification + Manual Console Check\n" +
  503. "• Session Cookie (may not work due to security)",
  504. "About Claude Balance Monitor",
  505. MessageBoxButtons.OK,
  506. MessageBoxIcon.Information
  507. );
  508. }
  509. private void ShowHide_Click(object sender, EventArgs e)
  510. {
  511. if (isVisible)
  512. {
  513. Hide();
  514. isVisible = false;
  515. }
  516. else
  517. {
  518. Show();
  519. WindowState = FormWindowState.Normal;
  520. isVisible = true;
  521. }
  522. }
  523. private void Settings_Click(object sender, EventArgs e)
  524. {
  525. var settingsForm = new SettingsForm(sessionToken);
  526. if (settingsForm.ShowDialog() == DialogResult.OK)
  527. {
  528. sessionToken = settingsForm.SessionToken;
  529. SaveConfiguration();
  530. _ = RefreshBalance();
  531. }
  532. }
  533. private void Exit_Click(object sender, EventArgs e)
  534. {
  535. refreshTimer?.Stop();
  536. notifyIcon?.Dispose();
  537. httpClient?.Dispose();
  538. Application.Exit();
  539. }
  540. // Mouse event handlers for dragging and clicking
  541. private void MainForm_MouseDown(object sender, MouseEventArgs e)
  542. {
  543. if (e.Button == MouseButtons.Left)
  544. {
  545. isDragging = true;
  546. dragStartPoint = e.Location;
  547. this.Cursor = Cursors.SizeAll;
  548. }
  549. else if (e.Button == MouseButtons.Right)
  550. {
  551. notifyIcon.ContextMenuStrip.Show(Cursor.Position);
  552. }
  553. }
  554. private void MainForm_MouseMove(object sender, MouseEventArgs e)
  555. {
  556. if (isDragging)
  557. {
  558. Point screenPoint = PointToScreen(e.Location);
  559. Location = new Point(screenPoint.X - dragStartPoint.X, screenPoint.Y - dragStartPoint.Y);
  560. }
  561. else
  562. {
  563. this.Cursor = currentBalance == -2 ? Cursors.Hand : Cursors.SizeAll;
  564. }
  565. }
  566. private void MainForm_MouseUp(object sender, MouseEventArgs e)
  567. {
  568. if (e.Button == MouseButtons.Left)
  569. {
  570. if (isDragging)
  571. {
  572. // Check if this was actually a drag or just a click
  573. var dragDistance = Math.Sqrt(Math.Pow(e.X - dragStartPoint.X, 2) + Math.Pow(e.Y - dragStartPoint.Y, 2));
  574. if (dragDistance < 5) // Less than 5 pixels = click, not drag
  575. {
  576. // This was a click, not a drag
  577. if (currentBalance == -2) // If showing "Check Console", open console on click
  578. {
  579. OpenConsole_Click(sender, e);
  580. }
  581. }
  582. }
  583. isDragging = false;
  584. this.Cursor = currentBalance == -2 ? Cursors.Hand : Cursors.Default;
  585. }
  586. }
  587. protected override void OnFormClosing(FormClosingEventArgs e)
  588. {
  589. if (e.CloseReason == CloseReason.UserClosing)
  590. {
  591. e.Cancel = true;
  592. Hide();
  593. isVisible = false;
  594. }
  595. else
  596. {
  597. base.OnFormClosing(e);
  598. }
  599. }
  600. protected override void Dispose(bool disposing)
  601. {
  602. if (disposing)
  603. {
  604. refreshTimer?.Dispose();
  605. notifyIcon?.Dispose();
  606. httpClient?.Dispose();
  607. }
  608. base.Dispose(disposing);
  609. }
  610. }
  611. }