diff --git a/windows/PopupWindow.xaml.cs b/windows/PopupWindow.xaml.cs new file mode 100644 index 0000000..34bcf1a --- /dev/null +++ b/windows/PopupWindow.xaml.cs @@ -0,0 +1,411 @@ +using ClaudeCheckerWindows.Controls; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +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; + + public PopupWindow() + { + InitializeComponent(); + DataContext = VM; + + VM.PropertyChanged += (_, e) => Dispatcher.InvokeAsync(() => OnVmChanged(e.PropertyName)); + Updater.PropertyChanged += (_, e) => Dispatcher.InvokeAsync(() => OnUpdaterChanged(e.PropertyName)); + + _clockTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) }; + _clockTimer.Tick += (_, _) => UpdateFooter(); + _clockTimer.Start(); + + InitSettings(); + RebuildCards(); + UpdateBannerState(); + UpdateFooter(); + + Deactivated += (_, _) => Hide(); + } + + // ── Card rendering ────────────────────────────────────────────── + + private void RebuildCards() + { + CardsPanel.Children.Clear(); + foreach (var limit in VM.Limits) + CardsPanel.Children.Add(BuildCard(limit)); + } + + private static UIElement BuildCard(AgentLimit limit) + { + var accent = new SolidColorBrush(Color.FromRgb(0xF9, 0x73, 0x16)); + + var gauge = new GaugeControl + { + Width = 76, Height = 76, Percent = limit.UsedPercent, Accent = accent, + }; + + var livePanel = new StackPanel { Orientation = Orientation.Horizontal }; + if (limit.IsLive) + { + livePanel.Children.Add(new Border + { + Background = new SolidColorBrush(Color.FromArgb(26, 0, 255, 0)), + CornerRadius = new CornerRadius(4), + Padding = new Thickness(6, 2, 6, 2), + Margin = new Thickness(0, 0, 6, 0), + Child = new TextBlock + { + Text = "Live", + FontSize = 10, + FontWeight = FontWeights.Medium, + Foreground = new SolidColorBrush(Colors.LightGreen), + } + }); + } + + var usageBadge = new Border + { + Background = new SolidColorBrush(Color.FromArgb(15, 255, 255, 255)), + CornerRadius = new CornerRadius(4), + Padding = new Thickness(6, 2, 6, 2), + Child = new TextBlock + { + Text = $"Usg: {limit.UsageLabel}", + FontSize = 10, + FontWeight = FontWeights.Medium, + Foreground = (SolidColorBrush)Application.Current.Resources["SecondaryBrush"], + } + }; + livePanel.Children.Add(usageBadge); + + var progressBar = new ProgressBar + { + Value = limit.UsedPercent, + Maximum = 100, + Height = 4, + Foreground = accent, + Background = new SolidColorBrush(Color.FromArgb(18, 255, 255, 255)), + BorderThickness = new Thickness(0), + Margin = new Thickness(0, 4, 0, 4), + }; + + var sparkline = new SparklineControl + { + Height = 28, + Data = limit.BurnHistory.Count > 0 ? limit.BurnHistory : null, + LineColor = Color.FromRgb(0xF9, 0x73, 0x16), + Margin = new Thickness(0, 8, 0, 0), + }; + + var infoPanel = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; + infoPanel.Children.Add(new Grid + { + Children = + { + new StackPanel + { + Orientation = Orientation.Horizontal, + Children = + { + MakeText("✦", 11, accent), + MakeText("Claude", 13, Brushes.White, FontWeights.SemiBold, new Thickness(6,0,0,0)), + MakeText($"· {VM.PlanLabel}", 11, + (SolidColorBrush)Application.Current.Resources["SecondaryBrush"], + margin: new Thickness(6,0,0,0)), + } + }.Also(s => Grid.SetColumn(s, 0)), + new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + Children = { /* live + usg */ }, + }.Also(s => { Grid.SetColumn(s, 1); ((StackPanel)s).Children.Add(livePanel); }), + }.Tap(g => { + var gc = (Grid)g.Parent ?? (Grid)Application.Current.MainWindow; + ((Grid)g.Parent!).ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + ((Grid)g.Parent!).ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + }), + }); + + // Simpler row approach + var headerRow = new Grid(); + headerRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + headerRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var leftStack = new StackPanel { Orientation = Orientation.Horizontal }; + leftStack.Children.Add(MakeText("✦", 11, accent)); + leftStack.Children.Add(MakeText("Claude", 13, Brushes.White, FontWeights.SemiBold, new Thickness(6, 0, 0, 0))); + leftStack.Children.Add(MakeText($"· {VM.PlanLabel}", 11, + (SolidColorBrush)Application.Current.Resources["SecondaryBrush"], margin: new Thickness(6, 0, 0, 0))); + + Grid.SetColumn(leftStack, 0); + Grid.SetColumn(livePanel, 1); + headerRow.Children.Add(leftStack); + headerRow.Children.Add(livePanel); + + infoPanel.Children.Clear(); + infoPanel.Children.Add(headerRow); + infoPanel.Children.Add(progressBar); + + var timeRow = new Grid(); + timeRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + timeRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var timeLeft = new StackPanel { Orientation = Orientation.Horizontal }; + timeLeft.Children.Add(MakeText("⏱ ", 10, (SolidColorBrush)Application.Current.Resources["SecondaryBrush"])); + timeLeft.Children.Add(MakeText(limit.TimeRemaining, 12, Brushes.White, FontWeights.Medium)); + + var resetText = MakeText( + $"Resets {limit.ResetDate:d MMM yyyy} at {limit.ResetDate:HH:mm}", + 11, (SolidColorBrush)Application.Current.Resources["SecondaryBrush"]); + + Grid.SetColumn(timeLeft, 0); + Grid.SetColumn(resetText, 1); + timeRow.Children.Add(timeLeft); + timeRow.Children.Add(resetText); + infoPanel.Children.Add(timeRow); + infoPanel.Children.Add(sparkline); + + var cardContent = new Grid { Margin = new Thickness(16, 12, 16, 12) }; + cardContent.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + cardContent.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(16, GridUnitType.Pixel) }); + cardContent.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + + Grid.SetColumn(gauge, 0); + Grid.SetColumn(infoPanel, 2); + cardContent.Children.Add(gauge); + cardContent.Children.Add(infoPanel); + + var card = new Border + { + Background = (SolidColorBrush)Application.Current.Resources["CardBrush"], + CornerRadius = new CornerRadius(8), + BorderBrush = new SolidColorBrush(Color.FromArgb(20, 255, 255, 255)), + BorderThickness = new Thickness(0.5), + Child = cardContent, + Margin = new Thickness(8, 0, 8, 0), + }; + + var sectionLabel = new TextBlock + { + Text = limit.WindowLabel, + Style = (Style)Application.Current.Resources["HeaderText"], + Margin = new Thickness(16, 10, 16, 4), + }; + + var section = new StackPanel(); + section.Children.Add(sectionLabel); + section.Children.Add(card); + return section; + } + + private static TextBlock MakeText(string text, double size, Brush fg, + FontWeight? weight = null, Thickness? margin = null) + => new() + { + Text = text, + FontSize = size, + Foreground = fg, + FontWeight = weight ?? FontWeights.Normal, + Margin = margin ?? new Thickness(0), + VerticalAlignment = VerticalAlignment.Center, + }; + + // ── 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)) + { + RebuildCards(); + UpdateFooter(); + } + } + + private void OnUpdaterChanged(string? prop) + { + if (prop is nameof(UpdateManager.UpdateAvailable) or nameof(UpdateManager.LatestVersion)) + UpdateBannerState(); + 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 UpdateBannerState() + { + var visible = Updater.UpdateAvailable; + UpdateBanner.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; + UpdateBannerDiv.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; + LastUpdatedText.Text = diff.TotalSeconds < 10 + ? "Updated just now" + : $"Updated {(int)diff.TotalSeconds}s ago"; + } + else + { + LastUpdatedText.Text = "Not yet updated"; + } + LoadingBar.Visibility = VM.IsLoading ? Visibility.Visible : Visibility.Collapsed; + } + + // ── Settings ───────────────────────────────────────────────────── + + private void InitSettings() + { + VersionLabel.Text = $"v{App.Updater.CurrentVersion}"; + BetaToggle.IsChecked = Updater.BetaChannel; + CheckUpdateButton.Content = Updater.UpdateAvailable ? "Install" : "Check"; + + RefreshPicker.ItemsSource = new[] + { + ("1 min", 60), ("2 min", 120), ("3 min", 180), + ("4 min", 240), ("5 min", 300), ("10 min", 600), + }; + RefreshPicker.DisplayMemberPath = "Item1"; + RefreshPicker.SelectedIndex = Array.FindIndex( + new[] { 60, 120, 180, 240, 300, 600 }, + s => s == VM.RefreshInterval).Let(i => i < 0 ? 1 : i); + + var signedIn = VM.IsSignedIn; + AuthPanel.Children.Clear(); + AuthPanel.Children.Add(new StackPanel + { + Orientation = Orientation.Horizontal, + Children = + { + new Ellipse + { + Width = 7, Height = 7, + Fill = signedIn ? Brushes.LimeGreen : Brushes.Orange, + Margin = new Thickness(0,0,6,0), + VerticalAlignment = VerticalAlignment.Center, + }, + new TextBlock + { + Text = signedIn ? $"Signed in — {VM.UserEmail}" : "Not signed in", + FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White, + VerticalAlignment = VerticalAlignment.Center, + } + } + }); + + SignOutButton.Visibility = signedIn ? Visibility.Visible : Visibility.Collapsed; + SignInButton.Content = signedIn ? "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 void Close_Click(object s, RoutedEventArgs e) => Hide(); + + 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 void SignIn_Click(object s, RoutedEventArgs e) + { + var login = new LoginWindow(); + login.Owner = this; + if (login.ShowDialog() == true) + { + _ = VM.RefreshAsync(); + InitSettings(); + } + } + + 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 (string _, 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"; + } +} + +// Minimal extension helpers to avoid repeating boilerplate +file static class Extensions +{ + public static T Also(this T t, Action f) { f(t); return t; } + public static TResult Let(this T t, Func f) => f(t); +} + +file class Ellipse : System.Windows.Shapes.Ellipse { }