diff --git a/SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs b/SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs index 5a30abc..24cd08a 100644 --- a/SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs +++ b/SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs @@ -3,11 +3,15 @@ using System.Diagnostics; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using LiveChartsCore; +using LiveChartsCore.SkiaSharpView; +using LiveChartsCore.SkiaSharpView.Painting; using Microsoft.Extensions.Logging; using Microsoft.Win32; using SharepointToolbox.Core.Models; using SharepointToolbox.Services; using SharepointToolbox.Services.Export; +using SkiaSharp; namespace SharepointToolbox.ViewModels.Tabs; @@ -33,6 +37,51 @@ public partial class StorageViewModel : FeatureViewModelBase [ObservableProperty] private int _folderDepth; + [ObservableProperty] + private bool _isDonutChart = true; + + private ObservableCollection _fileTypeMetrics = new(); + public ObservableCollection FileTypeMetrics + { + get => _fileTypeMetrics; + private set + { + _fileTypeMetrics = value; + OnPropertyChanged(); + UpdateChartSeries(); + } + } + + public bool HasChartData => FileTypeMetrics.Count > 0; + + private IEnumerable _pieChartSeries = Enumerable.Empty(); + public IEnumerable PieChartSeries + { + get => _pieChartSeries; + private set { _pieChartSeries = value; OnPropertyChanged(); } + } + + private IEnumerable _barChartSeries = Enumerable.Empty(); + public IEnumerable BarChartSeries + { + get => _barChartSeries; + private set { _barChartSeries = value; OnPropertyChanged(); } + } + + private Axis[] _barXAxes = Array.Empty(); + public Axis[] BarXAxes + { + get => _barXAxes; + private set { _barXAxes = value; OnPropertyChanged(); } + } + + private Axis[] _barYAxes = Array.Empty(); + public Axis[] BarYAxes + { + get => _barYAxes; + private set { _barYAxes = value; OnPropertyChanged(); } + } + public bool IsMaxDepth { get => FolderDepth >= 999; @@ -163,6 +212,22 @@ public partial class StorageViewModel : FeatureViewModelBase { Results = new ObservableCollection(flat); } + + // Collect file-type metrics for chart visualization + progress.Report(OperationProgress.Indeterminate("Scanning file types for chart...")); + var typeMetrics = await _storageService.CollectFileTypeMetricsAsync(ctx, progress, ct); + + if (Application.Current?.Dispatcher is { } chartDispatcher) + { + await chartDispatcher.InvokeAsync(() => + { + FileTypeMetrics = new ObservableCollection(typeMetrics); + }); + } + else + { + FileTypeMetrics = new ObservableCollection(typeMetrics); + } } protected override void OnTenantSwitched(TenantProfile profile) @@ -170,6 +235,7 @@ public partial class StorageViewModel : FeatureViewModelBase _currentProfile = profile; _hasLocalSiteOverride = false; Results = new ObservableCollection(); + FileTypeMetrics = new ObservableCollection(); SiteUrl = string.Empty; OnPropertyChanged(nameof(CurrentProfile)); ExportCsvCommand.NotifyCanExecuteChanged(); @@ -229,6 +295,88 @@ public partial class StorageViewModel : FeatureViewModelBase } } + partial void OnIsDonutChartChanged(bool value) + { + UpdateChartSeries(); + } + + private void UpdateChartSeries() + { + var metrics = FileTypeMetrics.ToList(); + OnPropertyChanged(nameof(HasChartData)); + + if (metrics.Count == 0) + { + PieChartSeries = Enumerable.Empty(); + BarChartSeries = Enumerable.Empty(); + BarXAxes = Array.Empty(); + BarYAxes = Array.Empty(); + return; + } + + // Take top 10 by size, aggregate the rest as "Other" + var top = metrics.Take(10).ToList(); + long otherSize = metrics.Skip(10).Sum(m => m.TotalSizeBytes); + int otherCount = metrics.Skip(10).Sum(m => m.FileCount); + + var chartItems = new List(top); + if (otherSize > 0) + chartItems.Add(new FileTypeMetric("Other", otherSize, otherCount)); + + // Pie/Donut series + double innerRadius = IsDonutChart ? 50 : 0; + PieChartSeries = chartItems.Select(m => new PieSeries + { + Values = new[] { m.TotalSizeBytes }, + Name = m.DisplayLabel, + InnerRadius = innerRadius, + DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Outer, + DataLabelsFormatter = point => m.DisplayLabel, + ToolTipLabelFormatter = point => + $"{m.DisplayLabel}: {FormatBytes(m.TotalSizeBytes)} ({m.FileCount} files)" + }).ToList(); + + // Bar chart series + BarChartSeries = new ISeries[] + { + new ColumnSeries + { + Values = chartItems.Select(m => m.TotalSizeBytes).ToArray(), + Name = "Size", + DataLabelsFormatter = point => + { + int idx = (int)point.Index; + return idx < chartItems.Count ? FormatBytes(chartItems[idx].TotalSizeBytes) : ""; + } + } + }; + + BarXAxes = new Axis[] + { + new Axis + { + Labels = chartItems.Select(m => m.DisplayLabel).ToArray(), + LabelsRotation = -45 + } + }; + + BarYAxes = new Axis[] + { + new Axis + { + Labeler = value => FormatBytes((long)value) + } + }; + } + + private static string FormatBytes(long bytes) + { + if (bytes < 1024) return $"{bytes} B"; + if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB"; + if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB"; + return $"{bytes / (1024.0 * 1024 * 1024):F2} GB"; + } + private static void FlattenNode(StorageNode node, int level, List result) { node.IndentLevel = level;