From 877d59bb262418204b5919a0124d87cc6d061c14 Mon Sep 17 00:00:00 2001 From: SuperDooper <37051355+superdooper86@users.noreply.github.com> Date: Fri, 8 May 2026 22:45:27 +0200 Subject: [PATCH] fix: restore PopupWindow; 1s footer timer; Xm Ys ago format --- windows/PopupWindow.xaml.cs | 557 +++++++++++++++++++++++++++++++++++- 1 file changed, 556 insertions(+), 1 deletion(-) diff --git a/windows/PopupWindow.xaml.cs b/windows/PopupWindow.xaml.cs index e79be9f..51d5585 100644 --- a/windows/PopupWindow.xaml.cs +++ b/windows/PopupWindow.xaml.cs @@ -1,2 +1,557 @@ using ClaudeCheckerWindows.Controls; -using Sy \ No newline at end of file +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Threading; + +namespace ClaudeCheckerWindows; + +public partial class PopupWindow : Window +{ + private static readonly UsageViewModel VM = App.ViewModel; + private static readonly UpdateManager Updater = App.Updater; + private readonly DispatcherTimer _clockTimer; + + [DllImport("dwmapi.dll")] + private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int value, int size); + + private void ApplyTitleBarTheme() + { + var hwnd = new WindowInteropHelper(this).Handle; + if (hwnd == IntPtr.Zero) return; + int dark = ThemeManager.IsDark ? 1 : 0; + DwmSetWindowAttribute(hwnd, 20, ref dark, sizeof(int)); + } + + public PopupWindow() + { + InitializeComponent(); + + VM.PropertyChanged += (_, e) => Dispatcher.InvokeAsync(() => OnVmChanged(e.PropertyName)); + Updater.PropertyChanged += (_, e) => Dispatcher.InvokeAsync(() => OnUpdaterChanged(e.PropertyName)); + + _clockTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _clockTimer.Tick += (_, _) => UpdateFooter(); + _clockTimer.Start(); + + InitSettings(); + RebuildCards(); + UpdateBannerState(); + UpdateFooter(); + + Closing += (_, e) => { e.Cancel = true; Hide(); }; + SourceInitialized += (_, _) => ApplyTitleBarTheme(); + ThemeManager.ThemeChanged += () => { ApplyTitleBarTheme(); RebuildCards(); }; + } + + // ── Card rendering ─────────────────────────────────────────────── + + private void RebuildCards() + { + CardsPanel.Children.Clear(); + foreach (var limit in VM.Limits) + CardsPanel.Children.Add(BuildCard(limit)); + + if (VM.ExtraUsage?.IsEnabled == true) + CardsPanel.Children.Add(BuildExtraUsageSection()); + + var fiveHour = VM.Limits.FirstOrDefault(l => l.Window == WindowKind.FiveHour); + if (fiveHour != null && fiveHour.BurnHistory.Count > 0) + CardsPanel.Children.Add(BuildDiarySection(fiveHour)); + } + + private static UIElement BuildCard(AgentLimit limit) + { + var accent = new SolidColorBrush(Color.FromRgb(0xF9, 0x73, 0x16)); + var text = (SolidColorBrush)Application.Current.Resources["TextBrush"]; + var secondary = (SolidColorBrush)Application.Current.Resources["SecondaryBrush"]; + var border = (SolidColorBrush)Application.Current.Resources["BorderBrush"]; + + var gauge = new GaugeControl { Width = 76, Height = 76, Percent = limit.UsedPercent, Accent = accent }; + + var liveBadge = new Border + { + Background = new SolidColorBrush(Color.FromArgb(30, 0, 160, 0)), + CornerRadius = new CornerRadius(4), + Padding = new Thickness(6, 2, 6, 2), + Margin = new Thickness(0, 0, 6, 0), + Visibility = limit.IsLive ? Visibility.Visible : Visibility.Collapsed, + Child = new TextBlock { Text = "Live", FontSize = 10, FontWeight = FontWeights.Medium, + Foreground = new SolidColorBrush(Color.FromRgb(0, 120, 0)) } + }; + + var usageBadge = new Border + { + Background = new SolidColorBrush(Color.FromArgb(18, 0, 0, 0)), + CornerRadius = new CornerRadius(4), + Padding = new Thickness(6, 2, 6, 2), + Child = new TextBlock { Text = $"Usg: {limit.UsageLabel}", FontSize = 10, + FontWeight = FontWeights.Medium, Foreground = secondary } + }; + + var badgeStack = new StackPanel { Orientation = Orientation.Horizontal }; + badgeStack.Children.Add(liveBadge); + badgeStack.Children.Add(usageBadge); + + var nameStack = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; + nameStack.Children.Add(new TextBlock { Text = "✶", FontSize = 11, Foreground = accent, VerticalAlignment = VerticalAlignment.Center }); + nameStack.Children.Add(new TextBlock { Text = "Claude", FontSize = 13, FontWeight = FontWeights.SemiBold, + Foreground = text, Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center }); + nameStack.Children.Add(new TextBlock { Text = $"· {VM.PlanLabel}", FontSize = 11, + Foreground = secondary, Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center }); + + var headerRow = new Grid(); + headerRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + headerRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + Grid.SetColumn(nameStack, 0); + Grid.SetColumn(badgeStack, 1); + headerRow.Children.Add(nameStack); + headerRow.Children.Add(badgeStack); + + var progress = new ProgressBar + { + Value = limit.UsedPercent, Maximum = 100, Height = 4, + Margin = new Thickness(0, 4, 0, 4), + Foreground = accent, + Background = new SolidColorBrush(Color.FromArgb(20, 0, 0, 0)), + BorderThickness = new Thickness(0), + }; + + // Left: time remaining + reset date inline + var timeLeft = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; + timeLeft.Children.Add(new TextBlock { Text = "⏱ ", FontSize = 10, Foreground = secondary, VerticalAlignment = VerticalAlignment.Center }); + timeLeft.Children.Add(new TextBlock { Text = limit.TimeRemaining, FontSize = 12, + FontWeight = FontWeights.Medium, Foreground = text, VerticalAlignment = VerticalAlignment.Center }); + timeLeft.Children.Add(new TextBlock + { + Text = $" Resets {limit.ResetDate:d MMM yyyy} at {limit.ResetDate:HH:mm}", + FontSize = 10, Foreground = secondary, VerticalAlignment = VerticalAlignment.Center + }); + + // Right: "after reset" when 0%, "today HH:MM" when refreshed today + string? refreshLabel = limit.UsedPercent == 0 + ? "after reset" + : VM.LastUpdated?.Date == DateTime.Today + ? "today " + VM.LastUpdated.Value.ToString("h:mm tt") + : null; + + UIElement timeRight; + if (refreshLabel != null) + { + var inner = new StackPanel { Orientation = Orientation.Horizontal }; + inner.Children.Add(new TextBlock { Text = "↗ ", FontSize = 10, Foreground = secondary, VerticalAlignment = VerticalAlignment.Center }); + inner.Children.Add(new TextBlock { Text = refreshLabel, FontSize = 10, FontWeight = FontWeights.Medium, + Foreground = secondary, VerticalAlignment = VerticalAlignment.Center }); + timeRight = new Border + { + Background = new SolidColorBrush(Color.FromArgb(18, 0, 0, 0)), + CornerRadius = new CornerRadius(4), + Padding = new Thickness(6, 2, 6, 2), + Child = inner + }; + } + else + { + timeRight = new TextBlock(); + } + + var timeRow = new Grid(); + timeRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + timeRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + Grid.SetColumn(timeLeft, 0); + Grid.SetColumn(timeRight, 1); + timeRow.Children.Add(timeLeft); + timeRow.Children.Add(timeRight); + + var infoPanel = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; + infoPanel.Children.Add(headerRow); + infoPanel.Children.Add(progress); + infoPanel.Children.Add(timeRow); + + var cardGrid = new Grid { Margin = new Thickness(16, 12, 16, 12) }; + cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(16, GridUnitType.Pixel) }); + cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + Grid.SetColumn(gauge, 0); + Grid.SetColumn(infoPanel, 2); + cardGrid.Children.Add(gauge); + cardGrid.Children.Add(infoPanel); + + var card = new Border + { + Background = (SolidColorBrush)Application.Current.Resources["CardBrush"], + CornerRadius = new CornerRadius(6), + BorderBrush = border, + BorderThickness = new Thickness(1), + Margin = new Thickness(12, 0, 12, 0), + Child = cardGrid, + }; + + var sectionLabel = new TextBlock + { + Text = limit.WindowLabel, + Style = (Style)Application.Current.Resources["HeaderText"], + Margin = new Thickness(16, 12, 16, 6), + }; + + var section = new StackPanel(); + section.Children.Add(sectionLabel); + section.Children.Add(card); + return section; + } + + private static UIElement BuildDiarySection(AgentLimit limit) + { + var secondary = (SolidColorBrush)Application.Current.Resources["SecondaryBrush"]; + var border = (SolidColorBrush)Application.Current.Resources["BorderBrush"]; + + var statsRow = new Grid { Margin = new Thickness(12, 10, 12, 6) }; + statsRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + statsRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + var samplesText = new TextBlock + { + Text = $"{limit.BurnHistory.Count} samples", + FontSize = 11, Foreground = secondary, VerticalAlignment = VerticalAlignment.Center + }; + var burnText = new TextBlock + { + Text = $"Avg burn rate: {limit.BurnRate:F1}%/h", + FontSize = 11, Foreground = secondary, VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(samplesText, 0); + Grid.SetColumn(burnText, 1); + statsRow.Children.Add(samplesText); + statsRow.Children.Add(burnText); + + var sparkline = new SparklineControl + { + Height = 40, + Margin = new Thickness(8, 0, 8, 8), + LineColor = Color.FromRgb(0xF9, 0x73, 0x16), + Data = limit.BurnHistory, + }; + + var cardContent = new StackPanel(); + cardContent.Children.Add(statsRow); + cardContent.Children.Add(sparkline); + + var card = new Border + { + Background = (SolidColorBrush)Application.Current.Resources["CardBrush"], + CornerRadius = new CornerRadius(6), + BorderBrush = border, + BorderThickness = new Thickness(1), + Margin = new Thickness(12, 0, 12, 0), + Child = cardContent, + }; + + var sectionLabel = new TextBlock + { + Text = "SESSION DIARY", + Style = (Style)Application.Current.Resources["HeaderText"], + Margin = new Thickness(16, 12, 16, 6), + }; + + var section = new StackPanel(); + section.Children.Add(sectionLabel); + section.Children.Add(card); + return section; + } + + private UIElement BuildExtraUsageSection() + { + var text = (SolidColorBrush)Application.Current.Resources["TextBrush"]; + var secondary = (SolidColorBrush)Application.Current.Resources["SecondaryBrush"]; + var border = (SolidColorBrush)Application.Current.Resources["BorderBrush"]; + var accent = new SolidColorBrush(Color.FromRgb(0xF9, 0x73, 0x16)); + + var overage = VM.Overage; + var prepaid = VM.Prepaid; + var currency = VM.ExtraUsage?.Currency ?? overage?.Currency ?? "$"; + + var spent = (overage?.UsedCredits / 100.0) ?? (VM.ExtraUsage?.UsedCredits / 100.0) ?? 0; + var limit = (overage?.MonthlyCreditLimit / 100.0) ?? (VM.ExtraUsage?.MonthlyLimit / 100.0) ?? 0; + var balance = (prepaid?.Amount / 100.0) ?? 0; + + bool unlimited = overage?.MonthlyCreditLimit == null && VM.ExtraUsage?.MonthlyLimit == null; + + UIElement MakeRow(string label, string value, bool highlight = false) + { + var row = new Grid { Margin = new Thickness(0, 3, 0, 3) }; + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + var lbl = new TextBlock { Text = label, FontSize = 12, Foreground = secondary }; + var val = new TextBlock { Text = value, FontSize = 12, FontWeight = FontWeights.Medium, + Foreground = highlight ? new SolidColorBrush(Color.FromRgb(0x4C, 0xAF, 0x50)) : text }; + Grid.SetColumn(lbl, 0); + Grid.SetColumn(val, 1); + row.Children.Add(lbl); + row.Children.Add(val); + return row; + } + + var contentPanel = new StackPanel { Margin = new Thickness(12, 10, 12, 10) }; + contentPanel.Children.Add(MakeRow("Spent", $"{currency} {spent:F2}", spent > 0)); + if (unlimited) contentPanel.Children.Add(MakeRow("Limit", "Unlimited")); + else if (limit > 0) contentPanel.Children.Add(MakeRow("Limit", $"{currency} {limit:F2}")); + if (balance > 0) contentPanel.Children.Add(MakeRow("Balance", $"{currency} {balance:F2}")); + + if (limit > 0) + { + var pct = Math.Min(spent / limit * 100, 100); + contentPanel.Children.Add(new ProgressBar + { + Value = pct, Maximum = 100, Height = 4, + Margin = new Thickness(0, 6, 0, 0), + Foreground = accent, + Background = new SolidColorBrush(Color.FromArgb(20, 0, 0, 0)), + BorderThickness = new Thickness(0), + }); + } + + var card = new Border + { + Background = (SolidColorBrush)Application.Current.Resources["CardBrush"], + CornerRadius = new CornerRadius(6), + BorderBrush = border, + BorderThickness = new Thickness(1), + Margin = new Thickness(12, 0, 12, 0), + Child = contentPanel, + }; + + var sectionLabel = new TextBlock + { + Text = "EXTRA USAGE CREDITS", + Style = (Style)Application.Current.Resources["HeaderText"], + Margin = new Thickness(16, 12, 16, 6), + }; + + var section = new StackPanel(); + section.Children.Add(sectionLabel); + section.Children.Add(card); + return section; + } + + // ── State updates ──────────────────────────────────────────────── + + private void OnVmChanged(string? prop) + { + if (prop is nameof(UsageViewModel.Limits) or nameof(UsageViewModel.IsLoading) + or nameof(UsageViewModel.LastUpdated) or nameof(UsageViewModel.ErrorMessage) + or nameof(UsageViewModel.ExtraUsage) or nameof(UsageViewModel.Overage) or nameof(UsageViewModel.Prepaid)) + { + RebuildCards(); + UpdateFooter(); + } + if (prop is nameof(UsageViewModel.IsSignedIn) or nameof(UsageViewModel.UserEmail)) + { + InitSettings(); + } + } + + private void OnUpdaterChanged(string? prop) + { + if (prop is nameof(UpdateManager.UpdateAvailable) or nameof(UpdateManager.LatestVersion)) + { + UpdateBannerState(); + UpdateAboutSection(); + } + if (prop is nameof(UpdateManager.DownloadProgress)) + UpdateProgress.Value = Updater.DownloadProgress * 100; + if (prop is nameof(UpdateManager.StatusMessage)) + UpdateStatusText.Text = Updater.StatusMessage; + if (prop is nameof(UpdateManager.UpdateComplete) && Updater.UpdateComplete) + InstallButton.Visibility = Visibility.Collapsed; + } + + private void UpdateAboutSection() + { + if (Updater.UpdateAvailable) + { + UpdateAvailableText.Text = $"v{Updater.LatestVersion} available"; + UpdateAvailableText.Visibility = Visibility.Visible; + CheckUpdateButton.Content = "Install"; + } + else + { + UpdateAvailableText.Visibility = Visibility.Collapsed; + CheckUpdateButton.Content = "Check"; + } + } + + private void UpdateBannerState() + { + var visible = Updater.UpdateAvailable; + UpdateBanner.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; + if (visible) UpdateBannerTitle.Text = $"Update available — v{Updater.LatestVersion}"; + } + + private void UpdateFooter() + { + if (VM.LastUpdated.HasValue) + { + var diff = DateTime.Now - VM.LastUpdated.Value; + string age; + if (diff.TotalSeconds < 10) + age = "just now"; + else if (diff.TotalMinutes < 1) + age = $"{(int)diff.TotalSeconds} sec ago"; + else + age = $"{(int)diff.TotalMinutes} min, {diff.Seconds} sec ago"; + LastUpdatedText.Text = $"Updated {age}"; + } + else + { + LastUpdatedText.Text = "Not yet updated"; + } + LoadingBar.Visibility = VM.IsLoading ? Visibility.Visible : Visibility.Collapsed; + } + + // ── Settings ───────────────────────────────────────────────────── + + private static readonly (string Label, int Seconds)[] RefreshIntervals = + [ + ("1 min", 60), + ("2 min", 120), + ("3 min", 180), + ("4 min", 240), + ("5 min", 300), + ("10 min", 600), + ]; + + private void InitSettings() + { + VersionLabel.Text = $"v{Updater.CurrentVersion}"; + BetaToggle.IsChecked = Updater.BetaChannel; + UpdateAboutSection(); + + RefreshPicker.SelectionChanged -= RefreshPicker_Changed; + RefreshPicker.Items.Clear(); + int selectIdx = 1; + for (int i = 0; i < RefreshIntervals.Length; i++) + { + var (label, seconds) = RefreshIntervals[i]; + RefreshPicker.Items.Add(new ComboBoxItem { Content = label, Tag = seconds }); + if (seconds == VM.RefreshInterval) selectIdx = i; + } + RefreshPicker.SelectedIndex = selectIdx; + RefreshPicker.SelectionChanged += RefreshPicker_Changed; + + AuthPanel.Children.Clear(); + var dot = new System.Windows.Shapes.Ellipse + { + Width = 7, Height = 7, Margin = new Thickness(0, 0, 6, 0), + Fill = VM.IsSignedIn ? Brushes.LimeGreen : Brushes.Orange, + VerticalAlignment = VerticalAlignment.Center, + }; + var label2 = new TextBlock + { + Text = VM.IsSignedIn ? $"Signed in — {VM.UserEmail}" : "Not signed in", + FontSize = 13, FontWeight = FontWeights.SemiBold, + Foreground = (SolidColorBrush)Application.Current.Resources["TextBrush"], + VerticalAlignment = VerticalAlignment.Center, + }; + var authRow = new StackPanel { Orientation = Orientation.Horizontal }; + authRow.Children.Add(dot); + authRow.Children.Add(label2); + AuthPanel.Children.Add(authRow); + + SignOutButton.Visibility = VM.IsSignedIn ? Visibility.Visible : Visibility.Collapsed; + SignInButton.Content = VM.IsSignedIn ? "Re-authenticate" : "Sign In"; + } + + // ── Event handlers ─────────────────────────────────────────────── + + private void Settings_Click(object s, RoutedEventArgs e) + { + InitSettings(); + MainPanel.Visibility = Visibility.Collapsed; + SettingsPanel.Visibility = Visibility.Visible; + UpdatePanel.Visibility = Visibility.Collapsed; + } + + private void BackFromSettings_Click(object s, RoutedEventArgs e) => ShowMain(); + + private void ShowMain() + { + MainPanel.Visibility = Visibility.Visible; + SettingsPanel.Visibility = Visibility.Collapsed; + UpdatePanel.Visibility = Visibility.Collapsed; + } + + private async void Refresh_Click(object s, RoutedEventArgs e) => await VM.RefreshAsync(); + + private void UpdateBanner_Click(object s, RoutedEventArgs e) => ShowUpdatePanel(); + + private void ShowUpdatePanel() + { + CurrentVerLabel.Text = $"v{Updater.CurrentVersion}"; + NewVerLabel.Text = $"v{Updater.LatestVersion}"; + ReleaseNotesText.Text = Updater.ReleaseNotes; + MainPanel.Visibility = Visibility.Collapsed; + SettingsPanel.Visibility = Visibility.Collapsed; + UpdatePanel.Visibility = Visibility.Visible; + } + + private void CloseUpdate_Click(object s, RoutedEventArgs e) => ShowMain(); + + private async void Install_Click(object s, RoutedEventArgs e) + { + NotNowButton.Visibility = Visibility.Collapsed; + UpdateStatusText.Visibility = Visibility.Visible; + UpdateProgress.Visibility = Visibility.Visible; + await Updater.DownloadAndInstallAsync(); + } + + private async void SignIn_Click(object s, RoutedEventArgs e) + { + var login = new LoginWindow { Owner = this }; + if (login.ShowDialog() == true) + { + await VM.LoadFromCacheAsync(); + InitSettings(); + ShowMain(); + + _ = Task.Run(async () => + { + await Task.Delay(3000); + await VM.RefreshAsync(); + await Application.Current.Dispatcher.InvokeAsync(InitSettings); + }); + } + Show(); + Activate(); + } + + private async void SignOut_Click(object s, RoutedEventArgs e) + { + await VM.SignOutAsync(); + InitSettings(); + } + + private void BetaToggle_Changed(object s, RoutedEventArgs e) + { + Updater.BetaChannel = BetaToggle.IsChecked == true; + _ = Updater.CheckForUpdatesAsync(); + } + + private void RefreshPicker_Changed(object s, SelectionChangedEventArgs e) + { + if (RefreshPicker.SelectedItem is ComboBoxItem item && item.Tag is int seconds) + { + VM.RefreshInterval = seconds; + ((App)Application.Current).ScheduleTimer(seconds); + } + } + + private async void CheckUpdate_Click(object s, RoutedEventArgs e) + { + if (Updater.UpdateAvailable) { ShowUpdatePanel(); return; } + CheckUpdateButton.Content = "…"; + await Updater.CheckForUpdatesAsync(); + CheckUpdateButton.Content = Updater.UpdateAvailable ? "Install" : "Check"; + } +}