Files

168 lines
6.4 KiB
C#

using System.ComponentModel;
namespace FluidExplorer.Services.Operations;
public enum FileOperationKind
{
Copy,
Move,
Delete,
Recycle,
Rename,
NewFolder
}
public enum JobState
{
Queued,
Running,
Paused,
Completed,
CompletedWithErrors,
Cancelled,
Failed
}
public enum ConflictPolicy
{
/// <summary>交给 UI 决定(ConflictResolver)。</summary>
Ask,
Replace,
Skip,
KeepBoth,
Merge
}
public enum ConflictResolution
{
Replace,
Skip,
KeepBoth,
Cancel
}
public sealed class ConflictInfo
{
public required string SourcePath { get; init; }
public required string DestinationPath { get; init; }
public bool SourceIsDirectory { get; init; }
public long SourceSize { get; init; }
public long DestinationSize { get; init; }
public DateTime SourceModifiedUtc { get; init; }
public DateTime DestinationModifiedUtc { get; init; }
/// <summary>true 表示用户勾选了"为后续所有冲突执行相同操作"。</summary>
public bool ApplyToAll { get; set; }
}
/// <summary>队列里的一个作业:可暂停/继续/取消,进度实时上报(含速度与剩余时间)。</summary>
public sealed class FileOperationJob : INotifyPropertyChanged
{
private JobState _state = JobState.Queued;
private int _completedItems;
private long _completedBytes;
private string? _currentItem;
private string? _error;
private double _bytesPerSecond;
private bool _isIndeterminate;
public Guid Id { get; } = Guid.NewGuid();
public required FileOperationKind Kind { get; init; }
public required string Title { get; init; }
public required IReadOnlyList<string> Sources { get; init; }
public string? Destination { get; init; }
public ConflictPolicy Policy { get; init; } = ConflictPolicy.Ask;
public bool PermanentDelete { get; init; }
public string? NewName { get; init; }
public DateTime StartedUtc { get; } = DateTime.UtcNow;
public JobState State
{
get => _state;
set => Set(ref _state, value);
}
public int TotalItems { get; set; }
public int CompletedItems { get => _completedItems; set => Set(ref _completedItems, value); }
public long TotalBytes { get; set; }
public long CompletedBytes { get => _completedBytes; set => Set(ref _completedBytes, value); }
public double BytesPerSecond { get => _bytesPerSecond; set => Set(ref _bytesPerSecond, value); }
public string? CurrentItem { get => _currentItem; set => Set(ref _currentItem, value); }
public string? Error { get => _error; set => Set(ref _error, value); }
/// <summary>大小为 0 的作业(纯重命名等)显示旋转指示器。</summary>
public bool IsIndeterminate { get => _isIndeterminate; set => Set(ref _isIndeterminate, value); }
public double Progress => TotalBytes > 0
? Math.Clamp((double)CompletedBytes / TotalBytes, 0, 1)
: (TotalItems > 0 ? Math.Clamp((double)CompletedItems / TotalItems, 0, 1) : 0);
public TimeSpan? Eta => BytesPerSecond > 1 && TotalBytes > CompletedBytes
? TimeSpan.FromSeconds((TotalBytes - CompletedBytes) / BytesPerSecond)
: null;
public bool CanPause => State is JobState.Running or JobState.Paused;
public bool CanCancel => State is JobState.Queued or JobState.Running or JobState.Paused;
public bool IsFinished => State is JobState.Completed or JobState.CompletedWithErrors or JobState.Cancelled or JobState.Failed;
public event PropertyChangedEventHandler? PropertyChanged;
internal void RaiseAll()
{
foreach (var name in new[] { nameof(State), nameof(CompletedItems), nameof(CompletedBytes), nameof(BytesPerSecond),
nameof(CurrentItem), nameof(Error), nameof(Progress), nameof(Eta),
nameof(CanPause), nameof(CanCancel), nameof(IsFinished), nameof(IsIndeterminate) })
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
private void Set<T>(ref T field, T value, [System.Runtime.CompilerServices.CallerMemberName] string? name = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public sealed class UndoEntry
{
public required string Description { get; init; }
public required FileOperationKind Kind { get; init; }
/// <summary>(原路径, 现路径) 对;撤销即反向搬运。</summary>
public required List<(string From, string To)> Moves { get; init; }
/// <summary>删除操作记录:回收站里的 $R 文件路径 → 原始路径。</summary>
public List<(string RecyclePath, string OriginalPath)> Deleted { get; init; } = [];
}
public sealed record OperationResult(bool Success, int Succeeded, int Failed, int Skipped, string? Error = null);
/// <summary>
/// 文件操作引擎:所有操作进入统一队列串行/并行执行,UI 永不阻塞。
/// 支持暂停、继续、取消、冲突策略、错误重试,以及一步撤销(Ctrl+Z)。
/// </summary>
public interface IFileOperationService
{
IReadOnlyList<FileOperationJob> Jobs { get; }
event EventHandler? JobsChanged;
/// <summary>UI 设置此回调以弹出冲突对话框;未设置时按 KeepBoth 处理。</summary>
Func<ConflictInfo, Task<ConflictResolution>>? ConflictResolver { get; set; }
FileOperationJob EnqueueCopy(IReadOnlyList<string> sources, string destination, ConflictPolicy policy = ConflictPolicy.Ask);
FileOperationJob EnqueueMove(IReadOnlyList<string> sources, string destination, ConflictPolicy policy = ConflictPolicy.Ask);
FileOperationJob EnqueueDelete(IReadOnlyList<string> paths, bool permanent = false);
FileOperationJob EnqueueRename(string path, string newName);
FileOperationJob EnqueueNewFolder(string parentDirectory, string name);
void Pause(Guid jobId);
void Resume(Guid jobId);
void Cancel(Guid jobId);
void ClearFinished();
bool CanUndo { get; }
string? UndoDescription { get; }
event EventHandler? UndoStackChanged;
Task<OperationResult> UndoAsync(CancellationToken cancellationToken = default);
/// <summary>计算源集合的总大小与条目数(后台执行,用于进度条与冲突提示)。</summary>
Task<(long Bytes, int Items)> MeasureAsync(IReadOnlyList<string> paths, CancellationToken cancellationToken);
}