From f3d8a50dc68b125cd41b8ccfaa1fd465898f6ba5 Mon Sep 17 00:00:00 2001 From: SuperDooper <37051355+superdooper86@users.noreply.github.com> Date: Fri, 8 May 2026 12:12:42 +0200 Subject: [PATCH 001/196] feat(windows): add ClaudeCheckerWindows.csproj --- windows/ClaudeCheckerWindows.csproj | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 windows/ClaudeCheckerWindows.csproj diff --git a/windows/ClaudeCheckerWindows.csproj b/windows/ClaudeCheckerWindows.csproj new file mode 100644 index 0000000..e53a685 --- /dev/null +++ b/windows/ClaudeCheckerWindows.csproj @@ -0,0 +1,39 @@ + + + + WinExe + net8.0-windows + true + true + enable + enable + ClaudeChecker + ClaudeCheckerWindows + Assets\icon.ico + 1.1.3 + false + x64 + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + -- 2.47.3 From c0ed68d023a0669a60cac60fccdf9c3ac743fdb7 Mon Sep 17 00:00:00 2001 From: SuperDooper <37051355+superdooper86@users.noreply.github.com> Date: Fri, 8 May 2026 12:12:43 +0200 Subject: [PATCH 002/196] feat(windows): add App.xaml --- windows/App.xaml | 102 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 windows/App.xaml diff --git a/windows/App.xaml b/windows/App.xaml new file mode 100644 index 0000000..3245b10 --- /dev/null +++ b/windows/App.xaml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + -- 2.47.3 From ceb5951f2453a9ef7abec0d3744cd71f5e7c5f50 Mon Sep 17 00:00:00 2001 From: SuperDooper <37051355+superdooper86@users.noreply.github.com> Date: Fri, 8 May 2026 12:12:44 +0200 Subject: [PATCH 003/196] feat(windows): add App.xaml.cs --- windows/App.xaml.cs | 142 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 windows/App.xaml.cs diff --git a/windows/App.xaml.cs b/windows/App.xaml.cs new file mode 100644 index 0000000..6d1359b --- /dev/null +++ b/windows/App.xaml.cs @@ -0,0 +1,142 @@ +using System; +using System.Drawing; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Threading; +using Application = System.Windows.Application; + +namespace ClaudeCheckerWindows; + +public partial class App : Application +{ + public static UsageViewModel ViewModel { get; } = new(); + public static UpdateManager Updater { get; } = new(); + + private NotifyIcon? _tray; + private PopupWindow? _popup; + private DispatcherTimer? _timer; + + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + SetupTray(); + ScheduleTimer(ViewModel.RefreshInterval); + + _ = Task.Run(async () => + { + await Task.Delay(1000); + await ViewModel.RefreshAsync(); + await Updater.CheckForUpdatesAsync(); + }); + } + + private void SetupTray() + { + _tray = new NotifyIcon + { + Text = "ClaudeChecker", + Visible = true, + Icon = LoadIcon(), + }; + _tray.MouseClick += (_, e) => + { + if (e.Button == MouseButtons.Left) + TogglePopup(); + }; + _tray.ContextMenuStrip = BuildContextMenu(); + + ViewModel.PropertyChanged += (_, e) => + { + if (e.PropertyName is nameof(UsageViewModel.Limits) or nameof(UsageViewModel.ShowInTaskbar)) + UpdateTrayText(); + }; + } + + private static Icon LoadIcon() + { + try { return new Icon("Assets/icon.ico"); } + catch { return SystemIcons.Application; } + } + + private ContextMenuStrip BuildContextMenu() + { + var menu = new ContextMenuStrip(); + menu.Items.Add("Show ClaudeChecker", null, (_, _) => ShowPopup()); + menu.Items.Add(new ToolStripSeparator()); + menu.Items.Add("Quit", null, (_, _) => Quit()); + menu.BackColor = System.Drawing.Color.FromArgb(40, 40, 40); + menu.ForeColor = System.Drawing.Color.White; + return menu; + } + + private void TogglePopup() + { + if (_popup == null || !_popup.IsVisible) + ShowPopup(); + else + _popup.Hide(); + } + + private void ShowPopup() + { + if (_popup == null) + { + _popup = new PopupWindow(); + _popup.Closed += (_, _) => _popup = null; + } + + PositionPopup(); + _popup.Show(); + _popup.Activate(); + } + + private void PositionPopup() + { + if (_popup == null) return; + var workArea = SystemParameters.WorkArea; + _popup.Left = workArea.Right - _popup.Width - 8; + _popup.Top = workArea.Bottom - _popup.Height - 8; + } + + public void ScheduleTimer(int seconds) + { + _timer?.Stop(); + _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(seconds) }; + _timer.Tick += async (_, _) => + { + await ViewModel.RefreshAsync(); + await Updater.CheckForUpdatesAsync(); + }; + _timer.Start(); + } + + private void UpdateTrayText() + { + if (_tray == null) return; + var limits = ViewModel.Limits; + if (ViewModel.ShowInTaskbar && limits.Count >= 2) + { + var fh = limits.Find(l => l.Window == WindowKind.FiveHour); + var sd = limits.Find(l => l.Window == WindowKind.SevenDay); + if (fh != null && sd != null && fh.IsLive) + { + _tray.Text = $"ClaudeChecker {(int)fh.UsedPercent}% {(int)sd.UsedPercent}%"; + return; + } + } + _tray.Text = "ClaudeChecker"; + } + + private void Quit() + { + _tray?.Dispose(); + Shutdown(); + } + + protected override void OnExit(ExitEventArgs e) + { + _tray?.Dispose(); + base.OnExit(e); + } +} -- 2.47.3 From e0c73211a03d80ea2a91897fdb30a804c0957f4f Mon Sep 17 00:00:00 2001 From: SuperDooper <37051355+superdooper86@users.noreply.github.com> Date: Fri, 8 May 2026 12:12:45 +0200 Subject: [PATCH 004/196] feat(windows): add Models.cs --- windows/Models.cs | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 windows/Models.cs diff --git a/windows/Models.cs b/windows/Models.cs new file mode 100644 index 0000000..60d13d4 --- /dev/null +++ b/windows/Models.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ClaudeCheckerWindows; + +public enum WindowKind { FiveHour, SevenDay } + +public class AgentLimit +{ + public WindowKind Window { get; init; } + public double UsedPercent { get; set; } + public string TimeRemaining { get; set; } = ""; + public DateTime ResetDate { get; set; } + public double BurnRate { get; set; } + public List BurnHistory { get; set; } = []; + public bool IsLive { get; set; } + public string UsageLabel => UsedPercent switch { < 33 => "low", < 66 => "med", _ => "high" }; + public string WindowLabel => Window == WindowKind.FiveHour ? "5 HOUR LIMIT" : "7 DAY LIMIT"; +} + +public class UsageResponse +{ + [JsonPropertyName("claude_ai_default_5h")] public WindowData? FiveHour { get; set; } + [JsonPropertyName("claude_ai_default")] public WindowData? SevenDay { get; set; } +} + +public class WindowData +{ + [JsonPropertyName("utilization")] public double Utilization { get; set; } + [JsonPropertyName("resets_at")] public string? ResetsAt { get; set; } +} + +public class BootstrapResponse +{ + [JsonPropertyName("account")] public AccountInfo? Account { get; set; } +} + +public class AccountInfo +{ + [JsonPropertyName("email_address")] public string? EmailAddress { get; set; } +} + +public class OverageSpendLimit +{ + [JsonPropertyName("monthly_credit_limit")] public double? MonthlyCreditLimit { get; set; } + [JsonPropertyName("used_credits")] public double? UsedCredits { get; set; } + [JsonPropertyName("currency")] public string? Currency { get; set; } +} + +public class PrepaidCredits +{ + [JsonPropertyName("amount")] public double? Amount { get; set; } + [JsonPropertyName("currency")] public string? Currency { get; set; } +} + +public class VersionInfo +{ + [JsonPropertyName("version")] public string Version { get; set; } = ""; + [JsonPropertyName("url")] public string Url { get; set; } = ""; + [JsonPropertyName("notes")] public string? Notes { get; set; } +} -- 2.47.3 From bb4c28c7515467fb49f88a5debb4b9d77a98c950 Mon Sep 17 00:00:00 2001 From: SuperDooper <37051355+superdooper86@users.noreply.github.com> Date: Fri, 8 May 2026 12:12:47 +0200 Subject: [PATCH 005/196] feat(windows): add UsageViewModel.cs --- windows/UsageViewModel.cs | 313 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 windows/UsageViewModel.cs diff --git a/windows/UsageViewModel.cs b/windows/UsageViewModel.cs new file mode 100644 index 0000000..9534d97 --- /dev/null +++ b/windows/UsageViewModel.cs @@ -0,0 +1,313 @@ +using Microsoft.Web.WebView2.Core; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading.Tasks; +using System.Windows; + +namespace ClaudeCheckerWindows; + +public class UsageViewModel : INotifyPropertyChanged +{ + private static readonly string OrgId = "daf626a9-4924-4ff3-ba98-23b523062f8e"; + private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true }; + + private List _limits = []; + private bool _isLoading; + private string? _errorMessage; + private bool _isSignedIn; + private string _userEmail = ""; + private string _planLabel = "Pro"; + private DateTime? _lastUpdated; + private OverageSpendLimit? _overage; + private PrepaidCredits? _prepaid; + + private readonly Dictionary> _burnHistory = []; + private const int MaxHistorySamples = 24; + + public List Limits { get => _limits; set => Set(ref _limits, value); } + public bool IsLoading { get => _isLoading; set => Set(ref _isLoading, value); } + public string? ErrorMessage { get => _errorMessage; set => Set(ref _errorMessage, value); } + public bool IsSignedIn { get => _isSignedIn; set => Set(ref _isSignedIn, value); } + public string UserEmail { get => _userEmail; set => Set(ref _userEmail, value); } + public string PlanLabel { get => _planLabel; set => Set(ref _planLabel, value); } + public DateTime? LastUpdated { get => _lastUpdated; set => Set(ref _lastUpdated, value); } + public OverageSpendLimit? Overage { get => _overage; set => Set(ref _overage, value); } + public PrepaidCredits? Prepaid { get => _prepaid; set => Set(ref _prepaid, value); } + + private int _refreshInterval = 120; + public int RefreshInterval + { + get => _refreshInterval; + set + { + Set(ref _refreshInterval, value); + Properties.Settings.Default.RefreshInterval = value; + Properties.Settings.Default.Save(); + } + } + + private bool _showInTaskbar = true; + public bool ShowInTaskbar + { + get => _showInTaskbar; + set + { + Set(ref _showInTaskbar, value); + Properties.Settings.Default.ShowInTaskbar = value; + Properties.Settings.Default.Save(); + } + } + + public UsageViewModel() + { + _refreshInterval = Properties.Settings.Default.RefreshInterval > 0 + ? Properties.Settings.Default.RefreshInterval : 120; + _showInTaskbar = Properties.Settings.Default.ShowInTaskbar; + LoadBurnHistory(); + LoadPlaceholders(); + } + + public async Task RefreshAsync() + { + await Application.Current.Dispatcher.InvokeAsync(() => IsLoading = true); + + try + { + var cookies = await GetCookiesAsync(); + if (cookies.Count == 0) + { + await Application.Current.Dispatcher.InvokeAsync(() => + { + IsSignedIn = false; + ErrorMessage = "Not signed in — click Sign In to authenticate."; + IsLoading = false; + }); + return; + } + + using var http = BuildClient(cookies); + + var usageTask = FetchAsync(http, $"https://claude.ai/api/organizations/{OrgId}/usage"); + var overageTask = FetchAsync(http, $"https://claude.ai/api/organizations/{OrgId}/overage_spend_limit"); + var prepaidTask = FetchAsync(http, $"https://claude.ai/api/organizations/{OrgId}/prepaid/credits"); + var emailTask = FetchEmailAsync(http); + + await Task.WhenAll(usageTask, overageTask, prepaidTask, emailTask); + + var usage = usageTask.Result; + var limits = BuildLimits(usage); + + foreach (var limit in limits) + { + var key = limit.Window.ToString(); + if (!_burnHistory.ContainsKey(key)) _burnHistory[key] = []; + _burnHistory[key].Add(limit.UsedPercent); + if (_burnHistory[key].Count > MaxHistorySamples) + _burnHistory[key].RemoveAt(0); + limit.BurnHistory = [.. _burnHistory[key]]; + } + SaveBurnHistory(); + + await Application.Current.Dispatcher.InvokeAsync(() => + { + Limits = limits; + Overage = overageTask.Result; + Prepaid = prepaidTask.Result; + UserEmail = emailTask.Result ?? UserEmail; + IsSignedIn = true; + ErrorMessage = null; + LastUpdated = DateTime.Now; + IsLoading = false; + }); + } + catch (Exception ex) + { + await Application.Current.Dispatcher.InvokeAsync(() => + { + ErrorMessage = ex.Message; + IsLoading = false; + }); + } + } + + public async Task SignOutAsync() + { + var env = await CoreWebView2Environment.CreateAsync(); + var dataManager = env.CreateCoreWebView2CookieManager(); + // Clear via settings — simplest approach is to delete the stored cookies + Properties.Settings.Default.CookieStore = ""; + Properties.Settings.Default.Save(); + + await Application.Current.Dispatcher.InvokeAsync(() => + { + IsSignedIn = false; + UserEmail = ""; + ErrorMessage = "Signed out."; + Limits = []; + }); + } + + // Cookie store: we persist cookies as JSON in settings after login + public static async Task> GetCookiesAsync() + { + var raw = Properties.Settings.Default.CookieStore; + if (string.IsNullOrEmpty(raw)) return []; + try + { + return JsonSerializer.Deserialize>(raw) + ?.Select(c => (c.Name, c.Value, c.Domain, c.Path)) + .ToList() ?? []; + } + catch { return []; } + } + + public static void SaveCookies(IEnumerable cookies) + { + var entries = cookies + .Where(c => c.Domain.Contains("claude.ai")) + .Select(c => new CookieEntry { Name = c.Name, Value = c.Value, Domain = c.Domain, Path = c.Path }) + .ToList(); + Properties.Settings.Default.CookieStore = JsonSerializer.Serialize(entries); + Properties.Settings.Default.Save(); + } + + private static HttpClient BuildClient(List<(string Name, string Value, string Domain, string Path)> cookies) + { + var handler = new HttpClientHandler { UseCookies = false }; + var http = new HttpClient(handler); + http.DefaultRequestHeaders.Add("accept", "application/json"); + var cookieHeader = string.Join("; ", cookies.Select(c => $"{c.Name}={c.Value}")); + http.DefaultRequestHeaders.Add("Cookie", cookieHeader); + return http; + } + + private static async Task FetchAsync(HttpClient http, string url) where T : class + { + try + { + var resp = await http.GetAsync(url); + if (!resp.IsSuccessStatusCode) return null; + var json = await resp.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(json, JsonOpts); + } + catch { return null; } + } + + private static async Task FetchEmailAsync(HttpClient http) + { + try + { + var resp = await http.GetAsync("https://claude.ai/api/bootstrap"); + if (!resp.IsSuccessStatusCode) return null; + var json = await resp.Content.ReadAsStringAsync(); + var boot = JsonSerializer.Deserialize(json, JsonOpts); + return boot?.Account?.EmailAddress; + } + catch { return null; } + } + + private List BuildLimits(UsageResponse? usage) + { + if (usage == null) return []; + var now = DateTime.Now; + var result = new List(); + + if (usage.FiveHour != null) + { + var reset = ParseDate(usage.FiveHour.ResetsAt, now); + var pct = Math.Clamp(usage.FiveHour.Utilization, 0, 100); + result.Add(new AgentLimit + { + Window = WindowKind.FiveHour, + UsedPercent = pct, + TimeRemaining = TimeLeft(reset, now), + ResetDate = reset, + BurnRate = pct / 5.0, + IsLive = true, + }); + } + + if (usage.SevenDay != null) + { + var reset = ParseDate(usage.SevenDay.ResetsAt, now); + var pct = Math.Clamp(usage.SevenDay.Utilization, 0, 100); + result.Add(new AgentLimit + { + Window = WindowKind.SevenDay, + UsedPercent = pct, + TimeRemaining = TimeLeft(reset, now), + ResetDate = reset, + BurnRate = pct / (7 * 24.0), + IsLive = true, + }); + } + + return result; + } + + private static DateTime ParseDate(string? s, DateTime fallback) + { + if (s == null) return fallback.AddHours(1); + return DateTime.TryParse(s, null, System.Globalization.DateTimeStyles.RoundtripKind, out var d) + ? d.ToLocalTime() : fallback.AddHours(1); + } + + private static string TimeLeft(DateTime reset, DateTime now) + { + var diff = reset - now; + if (diff <= TimeSpan.Zero) return "resetting..."; + if (diff.TotalDays >= 1) + return $"{(int)diff.TotalDays}d {diff.Hours}h"; + return $"{diff.Hours}h {diff.Minutes}m"; + } + + private void LoadPlaceholders() + { + var now = DateTime.Now; + Limits = + [ + new() { Window = WindowKind.FiveHour, UsedPercent = 0, TimeRemaining = "—", ResetDate = now.AddHours(1) }, + new() { Window = WindowKind.SevenDay, UsedPercent = 0, TimeRemaining = "—", ResetDate = now.AddDays(7) }, + ]; + } + + private void LoadBurnHistory() + { + try + { + var raw = Properties.Settings.Default.BurnHistory; + if (string.IsNullOrEmpty(raw)) return; + var saved = JsonSerializer.Deserialize>>(raw); + if (saved != null) + foreach (var kv in saved) _burnHistory[kv.Key] = kv.Value; + } + catch { } + } + + private void SaveBurnHistory() + { + Properties.Settings.Default.BurnHistory = JsonSerializer.Serialize(_burnHistory); + Properties.Settings.Default.Save(); + } + + public event PropertyChangedEventHandler? PropertyChanged; + private void Set(ref T field, T value, [CallerMemberName] string? name = null) + { + if (EqualityComparer.Default.Equals(field, value)) return; + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + + private class CookieEntry + { + public string Name { get; set; } = ""; + public string Value { get; set; } = ""; + public string Domain { get; set; } = ""; + public string Path { get; set; } = "/"; + } +} -- 2.47.3 From 605b11e849bd1795a0b942dd842a8f03cad95011 Mon Sep 17 00:00:00 2001 From: SuperDooper <37051355+superdooper86@users.noreply.github.com> Date: Fri, 8 May 2026 12:12:48 +0200 Subject: [PATCH 006/196] feat(windows): add UpdateManager.cs --- windows/UpdateManager.cs | 260 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 windows/UpdateManager.cs diff --git a/windows/UpdateManager.cs b/windows/UpdateManager.cs new file mode 100644 index 0000000..466a80f --- /dev/null +++ b/windows/UpdateManager.cs @@ -0,0 +1,260 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Collections.Generic; +using System.Windows; + +namespace ClaudeCheckerWindows; + +public class UpdateManager : INotifyPropertyChanged +{ + private const string StableUrl = "https://raw.githubusercontent.com/superdooper86/claudechecker/refs/heads/main/version.json"; + private const string BetaUrl = "https://raw.githubusercontent.com/superdooper86/claudechecker/refs/heads/main/version-beta.json"; + + private bool _updateAvailable; + private string _latestVersion = ""; + private string _releaseNotes = ""; + private string _downloadUrl = ""; + private bool _betaAvailable; + private string _latestBetaVersion = ""; + private bool _isDownloading; + private double _downloadProgress; + private string _statusMessage = ""; + private string? _updateError; + private bool _updateComplete; + + public bool UpdateAvailable { get => _updateAvailable; set => Set(ref _updateAvailable, value); } + public string LatestVersion { get => _latestVersion; set => Set(ref _latestVersion, value); } + public string ReleaseNotes { get => _releaseNotes; set => Set(ref _releaseNotes, value); } + public string DownloadUrl { get => _downloadUrl; set => Set(ref _downloadUrl, value); } + public bool BetaAvailable { get => _betaAvailable; set => Set(ref _betaAvailable, value); } + public string LatestBetaVersion { get => _latestBetaVersion; set => Set(ref _latestBetaVersion, value); } + public bool IsDownloading { get => _isDownloading; set => Set(ref _isDownloading, value); } + public double DownloadProgress { get => _downloadProgress; set => Set(ref _downloadProgress, value); } + public string StatusMessage { get => _statusMessage; set => Set(ref _statusMessage, value); } + public string? UpdateError { get => _updateError; set => Set(ref _updateError, value); } + public bool UpdateComplete { get => _updateComplete; set => Set(ref _updateComplete, value); } + + private bool _betaChannel; + public bool BetaChannel + { + get => _betaChannel; + set + { + Set(ref _betaChannel, value); + Properties.Settings.Default.BetaChannel = value; + Properties.Settings.Default.Save(); + } + } + + public string CurrentVersion => + System.Reflection.Assembly.GetExecutingAssembly() + .GetName().Version?.ToString(3) ?? "1.0.0"; + + public UpdateManager() + { + _betaChannel = Properties.Settings.Default.BetaChannel; + } + + public async Task CheckForUpdatesAsync() + { + using var http = new HttpClient(); + http.DefaultRequestHeaders.CacheControl = + new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true }; + + var stable = await FetchVersionAsync(http, StableUrl); + var beta = await FetchVersionAsync(http, BetaUrl); + + await Application.Current.Dispatcher.InvokeAsync(() => + { + if (stable != null && IsNewer(stable.Version, CurrentVersion)) + { + LatestVersion = stable.Version; + ReleaseNotes = stable.Notes ?? ""; + DownloadUrl = stable.Url; + UpdateAvailable = true; + } + else + { + UpdateAvailable = false; + LatestVersion = ""; + } + + if (beta != null && IsNewer(beta.Version, CurrentVersion)) + { + LatestBetaVersion = beta.Version; + BetaAvailable = true; + + if (BetaChannel && IsNewer(beta.Version, LatestVersion)) + { + LatestVersion = beta.Version; + ReleaseNotes = beta.Notes ?? ""; + DownloadUrl = beta.Url; + UpdateAvailable = true; + } + } + else + { + BetaAvailable = false; + LatestBetaVersion = ""; + } + }); + } + + private static async Task FetchVersionAsync(HttpClient http, string url) + { + try + { + var bust = $"{url}?t={DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"; + var json = await http.GetStringAsync(bust); + return JsonSerializer.Deserialize(json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + catch { return null; } + } + + public async Task DownloadAndInstallAsync() + { + if (string.IsNullOrEmpty(DownloadUrl)) return; + + IsDownloading = true; + UpdateError = null; + UpdateComplete = false; + StatusMessage = "Downloading…"; + + try + { + var tmpDir = Path.Combine(Path.GetTempPath(), $"CCUpdate_{Guid.NewGuid():N}"); + Directory.CreateDirectory(tmpDir); + var zipPath = Path.Combine(tmpDir, "ClaudeChecker.zip"); + + using var http = new HttpClient(); + var response = await http.GetAsync(DownloadUrl, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + + var total = response.Content.Headers.ContentLength ?? 0; + var received = 0L; + + await using (var stream = await response.Content.ReadAsStreamAsync()) + await using (var file = File.Create(zipPath)) + { + var buffer = new byte[81920]; + int read; + while ((read = await stream.ReadAsync(buffer)) > 0) + { + await file.WriteAsync(buffer.AsMemory(0, read)); + received += read; + if (total > 0) + DownloadProgress = (double)received / total * 0.8; + } + } + + StatusMessage = "Unpacking…"; + DownloadProgress = 0.85; + + var extractDir = Path.Combine(tmpDir, "extracted"); + ZipFile.ExtractToDirectory(zipPath, extractDir, overwriteFiles: true); + + // Find the installer exe + var newExe = FindExe(extractDir); + if (newExe == null) throw new Exception("ClaudeChecker.exe not found in update package."); + + DownloadProgress = 0.95; + StatusMessage = "Installing…"; + + // Write a batch script to replace the exe after we exit + var currentExe = Process.GetCurrentProcess().MainModule!.FileName; + var script = $""" + @echo off + timeout /t 2 /nobreak > nul + copy /Y "{newExe}" "{currentExe}" + start "" "{currentExe}" + rmdir /S /Q "{tmpDir}" + """; + + var scriptPath = Path.Combine(Path.GetTempPath(), "claudechecker_update.bat"); + await File.WriteAllTextAsync(scriptPath, script); + + DownloadProgress = 1.0; + StatusMessage = "Installed! Relaunching…"; + UpdateComplete = true; + + await Task.Delay(800); + + Process.Start(new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/C \"{scriptPath}\"", + CreateNoWindow = true, + UseShellExecute = false, + }); + + Application.Current.Dispatcher.Invoke(() => Application.Current.Shutdown()); + } + catch (Exception ex) + { + UpdateError = ex.Message; + IsDownloading = false; + StatusMessage = ""; + } + } + + private static string? FindExe(string dir) + { + foreach (var f in Directory.EnumerateFiles(dir, "ClaudeChecker.exe", SearchOption.AllDirectories)) + return f; + return null; + } + + public static bool IsNewer(string version, string current) + { + static (int[] Base, int[]? Pre) Parse(string v) + { + if (string.IsNullOrEmpty(v)) return ([], null); + var halves = v.Split('-', 2); + var baseP = Array.ConvertAll(halves[0].Split('.'), p => int.TryParse(p, out var n) ? n : 0); + int[]? pre = halves.Length > 1 + ? Array.ConvertAll(halves[1].Split('.'), p => int.TryParse(p, out var n) ? n : 0) + : null; + return (baseP, pre); + } + + var (ab, ap) = Parse(version); + var (bb, bp) = Parse(current); + + for (var i = 0; i < Math.Max(ab.Length, bb.Length); i++) + { + var av = i < ab.Length ? ab[i] : 0; + var bv = i < bb.Length ? bb[i] : 0; + if (av != bv) return av > bv; + } + + if (ap == null && bp != null) return true; + if (ap != null && bp == null) return false; + if (ap != null && bp != null) + { + for (var i = 0; i < Math.Max(ap.Length, bp.Length); i++) + { + var av = i < ap.Length ? ap[i] : 0; + var bv = i < bp.Length ? bp[i] : 0; + if (av != bv) return av > bv; + } + } + return false; + } + + public event PropertyChangedEventHandler? PropertyChanged; + private void Set(ref T field, T value, [CallerMemberName] string? name = null) + { + if (EqualityComparer.Default.Equals(field, value)) return; + field = value; + Application.Current.Dispatcher.InvokeAsync(() + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name))); + } +} -- 2.47.3 From 2277a29a3e1c5400640eb0617b531e36dc6b5265 Mon Sep 17 00:00:00 2001 From: SuperDooper <37051355+superdooper86@users.noreply.github.com> Date: Fri, 8 May 2026 12:12:49 +0200 Subject: [PATCH 007/196] feat(windows): add PopupWindow.xaml --- windows/PopupWindow.xaml | 251 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 windows/PopupWindow.xaml diff --git a/windows/PopupWindow.xaml b/windows/PopupWindow.xaml new file mode 100644 index 0000000..e585d7a --- /dev/null +++ b/windows/PopupWindow.xaml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + +