Add Git overlay, operation tools, and virtualized preview so large folders stay responsive.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
228
src/Explorer.Presentation/ViewModels/FolderSyncViewModel.cs
Normal file
228
src/Explorer.Presentation/ViewModels/FolderSyncViewModel.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user