From 1e33787cf596f2170176a1b07d6f85005f6ea589 Mon Sep 17 00:00:00 2001 From: Cezary Piatek Date: Sat, 22 Nov 2025 15:53:31 +0100 Subject: [PATCH 01/11] Show statistic in the main area --- .../ScriptRunner.GUI/Views/MainWindow.axaml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml b/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml index cb7b2d8..29f3752 100644 --- a/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml +++ b/src/ScriptRunner/ScriptRunner.GUI/Views/MainWindow.axaml @@ -50,7 +50,18 @@ - + + + + + + + + @@ -60,9 +71,6 @@ SelectedItem="{Binding SelectedRecentExecution, Mode=TwoWay}" ShowDatePicker="True"/> - - - From 732f91906f2fd3c712ce42f0aca9f56f6a79352a Mon Sep 17 00:00:00 2001 From: Cezary Piatek Date: Sat, 22 Nov 2025 15:58:05 +0100 Subject: [PATCH 02/11] Add paging to statistics --- .../ViewModels/StatisticsViewModel.cs | 95 ++++++++++++++++++- .../ScriptRunner.GUI/Views/Statistics.axaml | 29 +++++- 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/ScriptRunner/ScriptRunner.GUI/ViewModels/StatisticsViewModel.cs b/src/ScriptRunner/ScriptRunner.GUI/ViewModels/StatisticsViewModel.cs index ef9cee6..7bba3a3 100644 --- a/src/ScriptRunner/ScriptRunner.GUI/ViewModels/StatisticsViewModel.cs +++ b/src/ScriptRunner/ScriptRunner.GUI/ViewModels/StatisticsViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Windows.Input; using ReactiveUI; namespace ScriptRunner.GUI.ViewModels; @@ -9,10 +10,18 @@ namespace ScriptRunner.GUI.ViewModels; public class StatisticsViewModel : ReactiveObject { private readonly ObservableCollection _executionLog; + private List _allActions = new(); + private const int PageSize = 10; public StatisticsViewModel(ObservableCollection executionLog) { _executionLog = executionLog; + + // Initialize commands + NextPageCommand = ReactiveCommand.Create(NextPage, this.WhenAnyValue(x => x.CanGoNext)); + PreviousPageCommand = ReactiveCommand.Create(PreviousPage, this.WhenAnyValue(x => x.CanGoPrevious)); + GoToPageCommand = ReactiveCommand.Create(GoToPage); + RefreshStatistics(); } @@ -51,6 +60,78 @@ public int MaxWeek set => this.RaiseAndSetIfChanged(ref _maxWeek, value); } + private int _currentPage = 1; + public int CurrentPage + { + get => _currentPage; + set + { + this.RaiseAndSetIfChanged(ref _currentPage, value); + this.RaisePropertyChanged(nameof(CanGoNext)); + this.RaisePropertyChanged(nameof(CanGoPrevious)); + this.RaisePropertyChanged(nameof(PageInfo)); + UpdatePagedActions(); + } + } + + private int _totalPages = 1; + public int TotalPages + { + get => _totalPages; + set + { + this.RaiseAndSetIfChanged(ref _totalPages, value); + this.RaisePropertyChanged(nameof(CanGoNext)); + this.RaisePropertyChanged(nameof(PageInfo)); + } + } + + private int _totalActions = 0; + public int TotalActions + { + get => _totalActions; + set + { + this.RaiseAndSetIfChanged(ref _totalActions, value); + this.RaisePropertyChanged(nameof(PageInfo)); + } + } + + public bool CanGoNext => CurrentPage < TotalPages; + public bool CanGoPrevious => CurrentPage > 1; + + public string PageInfo => TotalActions > 0 + ? $"Page {CurrentPage} of {TotalPages} ({TotalActions} total actions)" + : "No actions found"; + + public ICommand NextPageCommand { get; } + public ICommand PreviousPageCommand { get; } + public ICommand GoToPageCommand { get; } + + private void NextPage() + { + if (CanGoNext) + { + CurrentPage++; + } + } + + private void PreviousPage() + { + if (CanGoPrevious) + { + CurrentPage--; + } + } + + private void GoToPage(int pageNumber) + { + if (pageNumber >= 1 && pageNumber <= TotalPages) + { + CurrentPage = pageNumber; + } + } + public void RefreshStatistics() { var now = DateTime.Now; @@ -161,7 +242,7 @@ private int GetIntensityLevel(int count) private void GenerateTopActions(List yearData) { - var topActions = yearData + _allActions = yearData .GroupBy(x => new { x.Source, x.Name }) .Select(g => new TopActionItem { @@ -170,7 +251,6 @@ private void GenerateTopActions(List yearData) ExecutionCount = g.Count() }) .OrderByDescending(x => x.ExecutionCount) - .Take(10) .Select((item, index) => { item.Rank = (index + 1).ToString(); @@ -178,7 +258,16 @@ private void GenerateTopActions(List yearData) }) .ToList(); - TopActions = topActions; + TotalActions = _allActions.Count; + TotalPages = TotalActions > 0 ? (int)Math.Ceiling((double)TotalActions / PageSize) : 1; + CurrentPage = 1; + UpdatePagedActions(); + } + + private void UpdatePagedActions() + { + var skip = (CurrentPage - 1) * PageSize; + TopActions = _allActions.Skip(skip).Take(PageSize).ToList(); } } diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/Statistics.axaml b/src/ScriptRunner/ScriptRunner.GUI/Views/Statistics.axaml index 44de8c4..2e6c85a 100644 --- a/src/ScriptRunner/ScriptRunner.GUI/Views/Statistics.axaml +++ b/src/ScriptRunner/ScriptRunner.GUI/Views/Statistics.axaml @@ -134,7 +134,7 @@ - @@ -212,6 +212,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/ActionsList.axaml.cs b/src/ScriptRunner/ScriptRunner.GUI/Views/ActionsList.axaml.cs index c7557e0..ffbd24b 100644 --- a/src/ScriptRunner/ScriptRunner.GUI/Views/ActionsList.axaml.cs +++ b/src/ScriptRunner/ScriptRunner.GUI/Views/ActionsList.axaml.cs @@ -1,18 +1,176 @@ -using Avalonia; +using System; +using System.Collections.Generic; +using System.Linq; using Avalonia.Controls; +using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; +using ScriptRunner.GUI.ViewModels; namespace ScriptRunner.GUI.Views; public partial class ActionsList : UserControl { + private Border? _previouslySelectedBorder; + private bool _isInternalSelection; + private readonly List _categoryBadges = new(); + public ActionsList() { InitializeComponent(); + this.DataContextChanged += OnDataContextChanged; + this.Loaded += OnLoaded; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void OnLoaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + // Give the UI time to render, then find all category badges + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + FindAndTrackAllCategoryBadges(); + }, Avalonia.Threading.DispatcherPriority.Loaded); + } + + private void OnDataContextChanged(object? sender, EventArgs e) + { + if (DataContext is MainWindowViewModel viewModel) + { + viewModel.PropertyChanged += (s, args) => + { + if (args.PropertyName == nameof(MainWindowViewModel.SelectedAction)) + { + // Only clear if this is an external selection change (not from our click handler) + if (!_isInternalSelection) + { + if (_previouslySelectedBorder != null) + { + _previouslySelectedBorder.Classes.Remove("selected"); + _previouslySelectedBorder = null; + } + } + } + else if (args.PropertyName == nameof(MainWindowViewModel.SelectedCategoryFilter)) + { + // Update grayed state of all category badges when selection changes + UpdateCategoryBadgesGrayedState(); + } + else if (args.PropertyName == nameof(MainWindowViewModel.AvailableCategories)) + { + // When categories change, re-scan for badges + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + _categoryBadges.Clear(); + FindAndTrackAllCategoryBadges(); + UpdateCategoryBadgesGrayedState(); + }, Avalonia.Threading.DispatcherPriority.Loaded); + } + }; + } + } + + private void ActionTile_OnTapped(object? sender, RoutedEventArgs e) + { + if (sender is Border border && border.DataContext is TaggedScriptConfig taggedScriptConfig) + { + if (DataContext is MainWindowViewModel viewModel) + { + // Set flag to prevent the PropertyChanged handler from clearing our selection + _isInternalSelection = true; + + try + { + // Remove selected class from previously selected border + if (_previouslySelectedBorder != null && _previouslySelectedBorder != border) + { + _previouslySelectedBorder.Classes.Remove("selected"); + } + + // Add selected class to clicked border immediately + border.Classes.Add("selected"); + _previouslySelectedBorder = border; + + // Set SelectedActionOrGroup to trigger the same behavior as tree view selection + viewModel.SelectedActionOrGroup = taggedScriptConfig; + } + finally + { + // Reset flag after selection is complete + _isInternalSelection = false; + } + } + } + } + + private void ClearSearch_Click(object? sender, RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel viewModel) + { + viewModel.ActionFilter = string.Empty; + } + } + + private void CategoryBadge_OnTapped(object? sender, RoutedEventArgs e) + { + if (sender is Border border && border.DataContext is string category) + { + // Track this badge if not already tracked + if (!_categoryBadges.Contains(border)) + { + _categoryBadges.Add(border); + } + + if (DataContext is MainWindowViewModel viewModel) + { + viewModel.SelectedCategoryFilter = category; + // Gray state will be updated by PropertyChanged handler + } + } + } + + private void UpdateCategoryBadgesGrayedState() + { + if (DataContext is not MainWindowViewModel viewModel) + return; + + var selectedCategory = viewModel.SelectedCategoryFilter; + var shouldGrayOut = !string.IsNullOrEmpty(selectedCategory) && selectedCategory != "All"; + + // Update all tracked badges + foreach (var badge in _categoryBadges.ToList()) + { + if (badge.DataContext is string category) + { + if (shouldGrayOut && category != selectedCategory) + { + badge.Classes.Add("grayed"); + } + else + { + badge.Classes.Remove("grayed"); + } + } + } + } + + private void FindAndTrackAllCategoryBadges() + { + // Find all Border elements with the categoryBadge class + var allBadges = this.GetVisualDescendants() + .OfType() + .Where(b => b.Classes.Contains("categoryBadge")) + .ToList(); + + foreach (var badge in allBadges) + { + if (!_categoryBadges.Contains(badge)) + { + _categoryBadges.Add(badge); + } + } + } } \ No newline at end of file