229 lines
6.4 KiB
C#
229 lines
6.4 KiB
C#
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<SyncProfile> Profiles { get; }
|
|
public ObservableCollection<SyncPreviewRow> Rows { get; }
|
|
public IReadOnlyList<SyncModeOption> 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);
|