168 lines
6.4 KiB
C#
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);
|
|
}
|