Files
FileTime2/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs
2023-07-21 22:15:48 +02:00

467 lines
18 KiB
C#

using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text.RegularExpressions;
using DeclarativeProperty;
using DynamicData;
using FileTime.App.Core.Interactions;
using FileTime.App.Core.Models;
using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.UserCommand;
using FileTime.App.Core.ViewModels;
using FileTime.Core.Command;
using FileTime.Core.Command.CreateContainer;
using FileTime.Core.Command.CreateElement;
using FileTime.Core.Command.Move;
using FileTime.Core.Extensions;
using FileTime.Core.Interactions;
using FileTime.Core.Models;
using FileTime.Core.Timeline;
using InitableService;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using CreateElementCommand = FileTime.App.Core.UserCommand.CreateElementCommand;
namespace FileTime.App.Core.Services.UserCommandHandler;
public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServiceBase
{
private ITabViewModel? _selectedTab;
private IDeclarativeProperty<IItemViewModel?>? _currentSelectedItem;
private readonly IUserCommandHandlerService _userCommandHandlerService;
private readonly IClipboardService _clipboardService;
private readonly IUserCommunicationService _userCommunicationService;
private readonly ILogger<ItemManipulationUserCommandHandlerService> _logger;
private readonly ITimelessContentProvider _timelessContentProvider;
private readonly ICommandScheduler _commandScheduler;
private readonly IServiceProvider _serviceProvider;
private readonly ISystemClipboardService _systemClipboardService;
private readonly BindedCollection<FullName>? _markedItems;
private IDeclarativeProperty<IContainer?>? _currentLocation;
public ItemManipulationUserCommandHandlerService(
IAppState appState,
IUserCommandHandlerService userCommandHandlerService,
IClipboardService clipboardService,
IUserCommunicationService userCommunicationService,
ILogger<ItemManipulationUserCommandHandlerService> logger,
ITimelessContentProvider timelessContentProvider,
ICommandScheduler commandScheduler,
IServiceProvider serviceProvider,
ISystemClipboardService systemClipboardService) : base(appState, timelessContentProvider)
{
_userCommandHandlerService = userCommandHandlerService;
_clipboardService = clipboardService;
_userCommunicationService = userCommunicationService;
_logger = logger;
_timelessContentProvider = timelessContentProvider;
_commandScheduler = commandScheduler;
_serviceProvider = serviceProvider;
_systemClipboardService = systemClipboardService;
SaveSelectedTab(t => _selectedTab = t);
SaveCurrentLocation(l => _currentLocation = l);
SaveCurrentSelectedItem(i => _currentSelectedItem = i);
_markedItems = appState.SelectedTab.Select(t => t?.MarkedItems).ToBindedCollection();
AddCommandHandlers(new IUserCommandHandler[]
{
new TypeUserCommandHandler<CopyCommand>(CopyAsync),
new TypeUserCommandHandler<DeleteCommand>(DeleteAsync),
new TypeUserCommandHandler<RenameCommand>(RenameAsync),
new TypeUserCommandHandler<MarkCommand>(MarkItemAsync),
new TypeUserCommandHandler<PasteCommand>(PasteAsync),
new TypeUserCommandHandler<CreateContainer>(CreateContainerAsync),
new TypeUserCommandHandler<CreateElementCommand>(CreateElementAsync),
new TypeUserCommandHandler<PasteFilesFromClipboardCommand>(PasteFilesFromClipboardAsync),
new TypeUserCommandHandler<CopyFilesToClipboardCommand>(CopyFilesToClipboardAsync),
});
}
private async Task CopyFilesToClipboardAsync()
{
var list = new List<FullName>();
if ((_markedItems?.Collection?.Count ?? 0) > 0)
{
list.AddRange(_markedItems!.Collection!);
}
else if(_currentSelectedItem?.Value?.BaseItem?.FullName is { } selectedItemName)
{
list.Add(selectedItemName);
}
if (list.Count > 0)
{
await _systemClipboardService.SetFilesAsync(list);
}
}
private async Task PasteFilesFromClipboardAsync(PasteFilesFromClipboardCommand command) =>
await (command.PasteMode switch
{
PasteMode.Merge => PasteFilesFromClipboardAsync(TransportMode.Merge),
PasteMode.Overwrite => PasteFilesFromClipboardAsync(TransportMode.Overwrite),
PasteMode.Skip => PasteFilesFromClipboardAsync(TransportMode.Skip),
_ => throw new ArgumentException($"Unknown {nameof(PasteMode)} value: {command.PasteMode}")
});
private async Task PasteFilesFromClipboardAsync(TransportMode mode)
{
if (_currentLocation?.Value?.FullName is not { }) return;
var files = (await _systemClipboardService.GetFilesAsync()).ToList();
var copyCommandFactory = _serviceProvider.GetRequiredService<FileTime.Core.Command.Copy.CopyCommandFactory>();
var copyCommand = copyCommandFactory.GenerateCommand(files, mode, _currentLocation.Value.FullName);
await AddCommandAsync(copyCommand);
}
private async Task MarkItemAsync()
{
if (_selectedTab == null || _currentSelectedItem?.Value?.BaseItem?.FullName == null) return;
_selectedTab.ToggleMarkedItem(_currentSelectedItem.Value.BaseItem.FullName);
await _userCommandHandlerService.HandleCommandAsync(MoveCursorDownCommand.Instance);
}
private Task CopyAsync()
{
_clipboardService.Clear();
_clipboardService.SetCommand<FileTime.Core.Command.Copy.CopyCommandFactory>();
if ((_markedItems?.Collection?.Count ?? 0) > 0)
{
foreach (var item in _markedItems!.Collection!)
{
_clipboardService.AddContent(item);
}
_selectedTab?.ClearMarkedItems();
}
else if (_currentSelectedItem?.Value?.BaseItem != null)
{
var item = _currentSelectedItem.Value.BaseItem;
_clipboardService.AddContent(
item.FullName
?? throw new ArgumentException($"{nameof(item.FullName)} can not be null.", nameof(item))
);
}
return Task.CompletedTask;
}
private async Task PasteAsync(PasteCommand command) =>
await (command.PasteMode switch
{
PasteMode.Merge => PasteAsync(TransportMode.Merge),
PasteMode.Overwrite => PasteAsync(TransportMode.Overwrite),
PasteMode.Skip => PasteAsync(TransportMode.Skip),
_ => throw new ArgumentException($"Unknown {nameof(PasteMode)} value: {command.PasteMode}")
});
private async Task PasteAsync(TransportMode mode)
{
if (_clipboardService.CommandFactoryType is null)
{
_userCommunicationService.ShowToastMessage("Clipboard is empty.");
return;
}
//TODO: check _currentLocation?.FullName
var commandFactory = (ITransportationCommandFactory) _serviceProvider.GetRequiredService(_clipboardService.CommandFactoryType);
var command = commandFactory.GenerateCommand(_clipboardService.Content, mode, _currentLocation?.Value?.FullName);
_clipboardService.Clear();
if (command is IRequireInputCommand requireInput) await requireInput.ReadInputs();
await AddCommandAsync(command);
}
private async Task CreateContainerAsync()
{
var containerNameInput = new TextInputElement("Container name");
await _userCommunicationService.ReadInputs(containerNameInput);
//TODO: message on empty result
var newContainerName = containerNameInput.Value;
if (_currentLocation?.Value?.FullName is null || newContainerName is null) return;
var command = _serviceProvider
.GetInitableResolver(_currentLocation.Value.FullName, newContainerName)
.GetRequiredService<CreateContainerCommand>();
await AddCommandAsync(command);
}
private async Task CreateElementAsync()
{
var containerNameInput = new TextInputElement("Element name");
await _userCommunicationService.ReadInputs(containerNameInput);
//TODO: message on empty result
var newContainerName = containerNameInput.Value;
if (_currentLocation?.Value?.FullName is null || newContainerName is null) return;
var command = _serviceProvider
.GetInitableResolver(_currentLocation.Value.FullName, newContainerName)
.GetRequiredService<FileTime.Core.Command.CreateElement.CreateElementCommand>();
await AddCommandAsync(command);
}
private async Task RenameAsync(RenameCommand command)
{
List<ItemToMove> itemsToMove = new();
if ((_markedItems?.Collection?.Count ?? 0) > 0)
{
BehaviorSubject<string> templateRegexValue = new(string.Empty);
BehaviorSubject<string> newNameSchemaValue = new(string.Empty);
var itemsToRename = new List<FullName>(_markedItems!.Collection!);
var itemPreviews = itemsToRename
.Select(item =>
{
var originalName = item.GetName();
var decoratedOriginalName = templateRegexValue.Select(templateRegex =>
{
try
{
if (string.IsNullOrWhiteSpace(templateRegex))
return new List<ItemNamePart> {new(originalName)};
var regex = new Regex(templateRegex);
var match = regex.Match(originalName);
if (!match.Success) return new List<ItemNamePart> {new(originalName)};
var matchGroups = match.Groups;
var indices = Enumerable.Empty<int>()
.Prepend(0)
.Concat(
((IList<Group>) match.Groups).Skip(1).SelectMany(g => new[] {g.Index, g.Index + g.Length})
)
.Append(originalName.Length)
.ToList();
var itemNameParts = new List<ItemNamePart>();
for (var i = 0; i < indices.Count - 1; i++)
{
var text = originalName.Substring(indices[i], indices[i + 1] - indices[i]);
itemNameParts.Add(new ItemNamePart(text, i % 2 == 1));
}
return itemNameParts;
}
catch
{
return new List<ItemNamePart> {new(originalName)};
}
}
);
var text2 = Observable.CombineLatest(
templateRegexValue,
newNameSchemaValue,
(templateRegex, newNameSchema) =>
{
try
{
if (string.IsNullOrWhiteSpace(templateRegex)
|| string.IsNullOrWhiteSpace(newNameSchema)) return new List<ItemNamePart> {new(originalName)};
var regex = new Regex(templateRegex);
var itemNameParts = GetItemNameParts(regex, originalName, newNameSchema);
return itemNameParts.ToList();
}
catch
{
return new List<ItemNamePart> {new(originalName)};
}
}
);
var preview = new DoubleTextPreview
{
Text1 = decoratedOriginalName,
Text2 = text2
};
return preview;
}
);
DoubleTextListPreview doubleTextListPreview = new();
doubleTextListPreview.Items.AddRange(itemPreviews);
var templateRegex = new TextInputElement("Template regex", string.Empty,
s => templateRegexValue.OnNext(s!));
var newNameSchema = new TextInputElement("New name schema", string.Empty,
s => newNameSchemaValue.OnNext(s!));
var success = await _userCommunicationService.ReadInputs(
new[] {templateRegex, newNameSchema},
new[] {doubleTextListPreview}
);
if (success)
{
if (templateRegex.Value is null)
{
//TODO messagebox
}
else if (newNameSchema.Value is null)
{
//TODO messagebox
}
else
{
var regex = new Regex(templateRegex.Value);
var itemsToMoveWithPath = itemsToRename
.Select(item =>
(
OriginalFullName: item,
NewName:
item.GetParent()!.GetChild(
string.Join(
"",
GetItemNameParts(regex, item.GetName(), newNameSchema.Value)
.Select(i => i.Text)
)
)
)
)
.Select(i => new ItemToMove(i.OriginalFullName, i.NewName));
itemsToMove.AddRange(itemsToMoveWithPath);
}
}
}
else
{
if (_currentSelectedItem?.Value?.BaseItem?.FullName is null) return;
var item = await _timelessContentProvider.GetItemByFullNameAsync(_currentSelectedItem.Value.BaseItem.FullName, PointInTime.Present);
if (item is null) return;
var renameInput = new TextInputElement("New name", item.Name);
if (await _userCommunicationService.ReadInputs(renameInput))
{
//TODO: should check these null forgivings...
var newPath = item.FullName!.GetParent()!.GetChild(renameInput.Value!);
itemsToMove.Add(new ItemToMove(item.FullName, newPath));
}
}
if (itemsToMove.Count > 0)
{
//TODO: name collision, probably on the input window at the new template name
//TODO: check if the name changed
var moveCommandFactory = _serviceProvider.GetRequiredService<MoveCommandFactory>();
var moveCommand = moveCommandFactory.GenerateCommand(itemsToMove);
await AddCommandAsync(moveCommand);
}
static IEnumerable<ItemNamePart> GetItemNameParts(Regex templateRegex, string originalName, string newNameSchema)
{
var match = templateRegex.Match(originalName);
if (!match.Success) return new List<ItemNamePart> {new(originalName)};
var matchGroups = match.Groups;
var newNameParts = Enumerable.Range(1, matchGroups.Count).Aggregate(
(IEnumerable<string>) new List<string> {newNameSchema},
(acc, i) =>
acc.SelectMany(item2 =>
item2
.Split($"/{i}/")
.SelectMany(e => new[] {e, $"/{i}/"})
.SkipLast(1)
)
);
var itemNameParts = newNameParts.Select(namePart =>
namePart.StartsWith("/")
&& namePart.EndsWith("/")
&& namePart.Length > 2
&& int.TryParse(namePart.AsSpan(1, namePart.Length - 2), out var index)
&& index > 0
&& index <= matchGroups.Count
? new ItemNamePart(matchGroups[index].Value, true)
: new ItemNamePart(namePart, false)
);
return itemNameParts;
}
}
private async Task DeleteAsync(DeleteCommand command)
{
IList<FullName>? itemsToDelete = null;
var shouldDelete = false;
string? questionText = null;
if ((_markedItems?.Collection?.Count ?? 0) > 0)
{
itemsToDelete = new List<FullName>(_markedItems!.Collection!);
}
else if (_currentSelectedItem?.Value?.BaseItem?.FullName is not null)
{
itemsToDelete = new List<FullName>()
{
_currentSelectedItem.Value.BaseItem.FullName
};
}
if ((itemsToDelete?.Count ?? 0) == 0) return;
if (itemsToDelete!.Count == 1)
{
var resolvedOnlyItem = await _timelessContentProvider.GetItemByFullNameAsync(itemsToDelete[0], PointInTime.Present);
if (resolvedOnlyItem is IContainer {AllowRecursiveDeletion: true} onlyContainer
&& onlyContainer.Items.Count > 0)
{
questionText = $"The container '{onlyContainer.DisplayName}' is not empty. Proceed with delete?";
}
else
{
shouldDelete = true;
}
}
else
{
shouldDelete = true;
}
if (itemsToDelete.Count == 0) return;
if (questionText is { })
{
var proceedDelete = await _userCommunicationService.ShowMessageBox(questionText!);
if (proceedDelete == MessageBoxResult.Cancel) return;
}
else if (!shouldDelete)
{
return;
}
var deleteCommand = _serviceProvider.GetRequiredService<FileTime.Core.Command.Delete.DeleteCommand>();
deleteCommand.HardDelete = command.IsHardDelete;
deleteCommand.ItemsToDelete.AddRange(itemsToDelete!);
await AddCommandAsync(deleteCommand);
_selectedTab?.ClearMarkedItems();
}
private async Task AddCommandAsync(ICommand command)
=> await _commandScheduler.AddCommand(command);
}