using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Explorer.Domain; using Explorer.FileOperations; namespace Explorer.Presentation.ViewModels; public sealed partial class FolderSyncViewModel : ObservableObject { private readonly FolderSyncService _sync; private OperationPlan? _plan; [ObservableProperty] private SyncProfile? _selected; [ObservableProperty] private string _name = "Photos backup"; [ObservableProperty] private string _sourcePath = ""; [ObservableProperty] private string _destPath = ""; [ObservableProperty] private SyncMode _mode = SyncMode.CopyUpdate; [ObservableProperty] private string _excludes = ""; [ObservableProperty] private bool _autoRun; [ObservableProperty] private string _status = "Save a profile, then Preview."; [ObservableProperty] private bool _canQueue; public FolderSyncViewModel(FolderSyncService sync) { _sync = sync; Profiles = []; Rows = []; Modes = [ new SyncModeOption("Copy / Update", SyncMode.CopyUpdate), new SyncModeOption("Mirror", SyncMode.Mirror) ]; } public ObservableCollection Profiles { get; } public ObservableCollection Rows { get; } public IReadOnlyList Modes { get; } public bool AutoRunEnabled => Mode == SyncMode.CopyUpdate; public async Task LoadAsync() { Profiles.Clear(); foreach (var profile in await _sync.ListAsync().ConfigureAwait(true)) { Profiles.Add(profile); } if (Profiles.Count > 0) { Selected = Profiles[0]; } else { NewProfile(); } } partial void OnSelectedChanged(SyncProfile? value) { if (value is null) { return; } Name = value.Name; SourcePath = value.SourcePath; DestPath = value.DestPath; Mode = value.Mode; Excludes = value.Excludes; AutoRun = value.AutoRun && value.Mode == SyncMode.CopyUpdate; ClearPlan("Profile loaded. Preview to see what would change."); } partial void OnModeChanged(SyncMode value) { OnPropertyChanged(nameof(AutoRunEnabled)); if (value != SyncMode.CopyUpdate) { AutoRun = false; } } [RelayCommand] public void NewProfile() { Selected = null; Name = "New sync"; SourcePath = ""; DestPath = ""; Mode = SyncMode.CopyUpdate; Excludes = ""; AutoRun = false; ClearPlan("New profile. Choose folders and Save."); } [RelayCommand] public async Task SaveAsync() { if (string.IsNullOrWhiteSpace(SourcePath) || string.IsNullOrWhiteSpace(DestPath)) { Status = "Choose a source folder and a destination folder."; return; } var profile = CurrentProfile(); profile.Id = await _sync.SaveAsync(profile).ConfigureAwait(true); var existing = Profiles.FirstOrDefault(p => p.Id == profile.Id); if (existing is not null) { var index = Profiles.IndexOf(existing); Profiles[index] = profile; } else { Profiles.Add(profile); } Selected = profile; Status = "Profile saved."; } [RelayCommand] public async Task DeleteAsync() { if (Selected is null || Selected.Id <= 0) { NewProfile(); return; } await _sync.DeleteAsync(Selected.Id).ConfigureAwait(true); Profiles.Remove(Selected); if (Profiles.Count > 0) { Selected = Profiles[0]; } else { NewProfile(); } Status = "Profile removed."; } [RelayCommand] public async Task AnalyzeAsync() { var profile = CurrentProfile(); Status = "Analyzing…"; CanQueue = false; var plan = await Task.Run(() => _sync.PreviewAsync(profile)).ConfigureAwait(true); ApplyPlan(plan); } [RelayCommand] public async Task QueueAsync() { if (_plan is null || !_plan.CanEnqueue) { Status = "Preview first. Nothing to queue."; return; } var profile = CurrentProfile(); if (profile.Id <= 0) { await SaveAsync().ConfigureAwait(true); profile = CurrentProfile(); } var plan = await _sync.EnqueueAsync(profile, _plan).ConfigureAwait(true); ApplyPlan(plan); if (plan.CanEnqueue) { Status = profile.LastStatus ?? "Queued."; CanQueue = false; } } public SyncProfile CurrentProfile() => new() { Id = Selected?.Id ?? 0, Name = string.IsNullOrWhiteSpace(Name) ? "Sync" : Name.Trim(), SourcePath = SourcePath.Trim(), DestPath = DestPath.Trim(), Mode = Mode, Excludes = Excludes ?? "", AutoRun = AutoRun && Mode == SyncMode.CopyUpdate, SourceVolumeGuid = Selected?.SourceVolumeGuid, DestVolumeGuid = Selected?.DestVolumeGuid, CreatedUtc = Selected?.CreatedUtc ?? DateTimeOffset.UtcNow, LastRunUtc = Selected?.LastRunUtc, LastStatus = Selected?.LastStatus }; private void ApplyPlan(OperationPlan plan) { _plan = plan; Rows.Clear(); foreach (var row in plan.SyncPreview) { Rows.Add(row); } CanQueue = plan.CanEnqueue; var errors = plan.Issues.Count(i => i.Severity == PlanIssueSeverity.Error); var warnings = plan.Issues.Count(i => i.Severity == PlanIssueSeverity.Warning); Status = errors > 0 ? plan.Issues.First(i => i.Severity == PlanIssueSeverity.Error).Message : plan.Operations.Count == 0 ? warnings > 0 ? $"Nothing to queue · {warnings} skipped" : "Folders already match." : $"{plan.Operations.Count} operations · {warnings} skipped"; } private void ClearPlan(string status) { _plan = null; Rows.Clear(); CanQueue = false; Status = status; } } public sealed record SyncModeOption(string Label, SyncMode Mode);