Files
Sharepoint-Toolbox/SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs
Dev 6a2e4d1d89 feat(06-04): update single-site tab VMs for global site consumption
- StorageViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- SearchViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- DuplicatesViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- FolderStructureViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- All four VMs pre-fill SiteUrl from first global site; local typing sets override flag
- Tenant switch resets _hasLocalSiteOverride in all four VMs
2026-04-07 10:08:19 +02:00

192 lines
6.9 KiB
C#

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Services;
using SharepointToolbox.Services.Export;
namespace SharepointToolbox.ViewModels.Tabs;
/// <summary>Flat display row wrapping a DuplicateItem with its group name.</summary>
public class DuplicateRow
{
public string GroupName { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Path { get; set; } = string.Empty;
public string Library { get; set; } = string.Empty;
public long? SizeBytes { get; set; }
public DateTime? Created { get; set; }
public DateTime? Modified { get; set; }
public int? FolderCount { get; set; }
public int? FileCount { get; set; }
public int GroupSize { get; set; }
}
public partial class DuplicatesViewModel : FeatureViewModelBase
{
private readonly IDuplicatesService _duplicatesService;
private readonly ISessionManager _sessionManager;
private readonly DuplicatesHtmlExportService _htmlExportService;
private readonly ILogger<FeatureViewModelBase> _logger;
private TenantProfile? _currentProfile;
private bool _hasLocalSiteOverride;
private IReadOnlyList<DuplicateGroup> _lastGroups = Array.Empty<DuplicateGroup>();
[ObservableProperty] private string _siteUrl = string.Empty;
[ObservableProperty] private bool _modeFiles = true;
[ObservableProperty] private bool _modeFolders;
[ObservableProperty] private bool _matchSize = true;
[ObservableProperty] private bool _matchCreated;
[ObservableProperty] private bool _matchModified;
[ObservableProperty] private bool _matchSubfolders;
[ObservableProperty] private bool _matchFileCount;
[ObservableProperty] private bool _includeSubsites;
[ObservableProperty] private string _library = string.Empty;
private ObservableCollection<DuplicateRow> _results = new();
public ObservableCollection<DuplicateRow> Results
{
get => _results;
private set
{
_results = value;
OnPropertyChanged();
ExportHtmlCommand.NotifyCanExecuteChanged();
}
}
public IAsyncRelayCommand ExportHtmlCommand { get; }
public TenantProfile? CurrentProfile => _currentProfile;
public DuplicatesViewModel(
IDuplicatesService duplicatesService,
ISessionManager sessionManager,
DuplicatesHtmlExportService htmlExportService,
ILogger<FeatureViewModelBase> logger)
: base(logger)
{
_duplicatesService = duplicatesService;
_sessionManager = sessionManager;
_htmlExportService = htmlExportService;
_logger = logger;
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
}
protected override void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
{
if (_hasLocalSiteOverride) return;
SiteUrl = sites.Count > 0 ? sites[0].Url : string.Empty;
}
partial void OnSiteUrlChanged(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
_hasLocalSiteOverride = false;
if (GlobalSites.Count > 0)
SiteUrl = GlobalSites[0].Url;
}
else if (GlobalSites.Count == 0 || value != GlobalSites[0].Url)
{
_hasLocalSiteOverride = true;
}
}
protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
{
if (_currentProfile == null)
{
StatusMessage = "No tenant selected. Please connect to a tenant first.";
return;
}
if (string.IsNullOrWhiteSpace(SiteUrl))
{
StatusMessage = "Please enter a site URL.";
return;
}
var siteProfile = new TenantProfile
{
TenantUrl = SiteUrl.TrimEnd('/'),
ClientId = _currentProfile.ClientId,
Name = _currentProfile.Name
};
var ctx = await _sessionManager.GetOrCreateContextAsync(siteProfile, ct);
var opts = new DuplicateScanOptions(
Mode: ModeFiles ? "Files" : "Folders",
MatchSize: MatchSize,
MatchCreated: MatchCreated,
MatchModified: MatchModified,
MatchSubfolderCount: MatchSubfolders,
MatchFileCount: MatchFileCount,
IncludeSubsites: IncludeSubsites,
Library: string.IsNullOrWhiteSpace(Library) ? null : Library
);
var groups = await _duplicatesService.ScanDuplicatesAsync(ctx, opts, progress, ct);
_lastGroups = groups;
// Flatten groups to display rows
var rows = groups
.SelectMany(g => g.Items.Select(item => new DuplicateRow
{
GroupName = g.Name,
Name = item.Name,
Path = item.Path,
Library = item.Library,
SizeBytes = item.SizeBytes,
Created = item.Created,
Modified = item.Modified,
FolderCount = item.FolderCount,
FileCount = item.FileCount,
GroupSize = g.Items.Count
}))
.ToList();
if (Application.Current?.Dispatcher is { } dispatcher)
await dispatcher.InvokeAsync(() => Results = new ObservableCollection<DuplicateRow>(rows));
else
Results = new ObservableCollection<DuplicateRow>(rows);
}
protected override void OnTenantSwitched(Core.Models.TenantProfile profile)
{
_currentProfile = profile;
_hasLocalSiteOverride = false;
Results = new ObservableCollection<DuplicateRow>();
_lastGroups = Array.Empty<DuplicateGroup>();
SiteUrl = string.Empty;
OnPropertyChanged(nameof(CurrentProfile));
ExportHtmlCommand.NotifyCanExecuteChanged();
}
internal void SetCurrentProfile(TenantProfile profile) => _currentProfile = profile;
private bool CanExport() => _lastGroups.Count > 0;
private async Task ExportHtmlAsync()
{
if (_lastGroups.Count == 0) return;
var dialog = new SaveFileDialog
{
Title = "Export duplicates report to HTML",
Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*",
DefaultExt = "html",
FileName = "duplicates_report"
};
if (dialog.ShowDialog() != true) return;
try
{
await _htmlExportService.WriteAsync(_lastGroups, dialog.FileName, CancellationToken.None);
Process.Start(new ProcessStartInfo(dialog.FileName) { UseShellExecute = true });
}
catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "HTML export failed."); }
}
}