TimeTravel

This commit is contained in:
2022-01-31 23:13:39 +01:00
parent 80570d8895
commit c2dcb49016
78 changed files with 2294 additions and 363 deletions

View File

@@ -0,0 +1,9 @@
namespace FileTime.Core.Command
{
public enum CanCommandRun
{
True,
False,
Forceable
}
}

View File

@@ -1,3 +1,5 @@
using FileTime.Core.Timeline;
namespace FileTime.Core.Command
{
public class CommandExecutor
@@ -9,15 +11,15 @@ namespace FileTime.Core.Command
_commandHandlers = commandHandlers.ToList();
}
public void ExecuteCommand(ICommand command)
public async Task ExecuteCommandAsync(ICommand command, TimeRunner timeRunner)
{
if (command is IExecutableCommand executableCommand)
{
executableCommand.Execute();
await executableCommand.Execute(timeRunner);
}
else
{
_commandHandlers.Find(c => c.CanHandle(command))?.Execute(command);
await _commandHandlers.Find(c => c.CanHandle(command))?.ExecuteAsync(command, timeRunner);
}
}
}

View File

@@ -5,59 +5,115 @@ namespace FileTime.Core.Command
{
public class CopyCommand : ITransportationCommand
{
public IList<IAbsolutePath> Sources { get; } = new List<IAbsolutePath>();
private Action<AbsolutePath, AbsolutePath>? _copyOperation;
private Func<IContainer, string, Task<IContainer>>? _createContainer;
private TimeRunner? _timeRunner;
public IList<AbsolutePath>? Sources { get; } = new List<AbsolutePath>();
public IContainer? Target { get; set; }
public TransportMode TransportMode { get; set; } = TransportMode.Merge;
public TransportMode? TransportMode { get; set; } = Command.TransportMode.Merge;
public PointInTime SimulateCommand(PointInTime delta)
public async Task<PointInTime> SimulateCommand(PointInTime startPoint)
{
throw new NotImplementedException();
if (Sources == null) throw new ArgumentException(nameof(Sources) + " can not be null");
if (Target == null) throw new ArgumentException(nameof(Target) + " can not be null");
if (TransportMode == null) throw new ArgumentException(nameof(TransportMode) + " can not be null");
var newDiffs = new List<Difference>();
_copyOperation = (_, to) =>
{
var target = to.GetParentAsAbsolutePath().Resolve();
newDiffs.Add(new Difference(
target is IElement
? DifferenceItemType.Element
: DifferenceItemType.Container,
DifferenceActionType.Create,
to
));
};
_createContainer = async (IContainer target, string name) =>
{
var newContainerDiff = new Difference(
DifferenceItemType.Container,
DifferenceActionType.Create,
AbsolutePath.FromParentAndChildName(target, name)
);
newDiffs.Add(newContainerDiff);
return (IContainer)(await newContainerDiff.AbsolutePath.Resolve())!;
};
await DoCopy(Sources, Target, TransportMode.Value);
return startPoint.WithDifferences(newDiffs);
}
public async Task Execute(Action<IAbsolutePath, IAbsolutePath> copy)
public async Task Execute(Action<AbsolutePath, AbsolutePath> copy, TimeRunner timeRunner)
{
await DoCopy(Sources, Target, TransportMode, copy);
if (Sources == null) throw new ArgumentException(nameof(Sources) + " can not be null");
if (Target == null) throw new ArgumentException(nameof(Target) + " can not be null");
if (TransportMode == null) throw new ArgumentException(nameof(TransportMode) + " can not be null");
_copyOperation = copy;
_createContainer = async (IContainer target, string name) => await target.CreateContainer(name);
_timeRunner = timeRunner;
await DoCopy(Sources, Target, TransportMode.Value);
}
private async Task DoCopy(IEnumerable<IAbsolutePath> sources, IContainer target, TransportMode transportMode, Action<IAbsolutePath, IAbsolutePath> copy)
private async Task DoCopy(
IEnumerable<AbsolutePath> sources,
IContainer target,
TransportMode transportMode)
{
if (_copyOperation == null) throw new ArgumentException("No copy operation were given.");
if (_createContainer == null) throw new ArgumentException("No container creation function were given.");
foreach (var source in sources)
{
var item = await source.ContentProvider.GetByPath(source.Path);
var item = await source.Resolve();
if (item is IContainer container)
{
var targetContainer = (await target.GetContainers())?.FirstOrDefault(d => d.Name == container.Name) ?? (await target.CreateContainer(container.Name)!);
var targetContainer = (await target.GetContainers())?.FirstOrDefault(d => d.Name == container.Name) ?? (await _createContainer?.Invoke(target, container.Name)!);
var childDirectories = (await container.GetContainers())!.Select(d => new AbsolutePath(item.Provider, d.FullName!));
var childFiles = (await container.GetElements())!.Select(f => new AbsolutePath(item.Provider, f.FullName!));
var childDirectories = (await container.GetContainers())!.Select(d => new AbsolutePath(d));
var childFiles = (await container.GetElements())!.Select(f => new AbsolutePath(f));
await DoCopy(childDirectories.Concat(childFiles), targetContainer, transportMode, copy);
await DoCopy(childDirectories.Concat(childFiles), targetContainer, transportMode);
_timeRunner?.RefreshContainer.InvokeAsync(this, new AbsolutePath(container));
}
else if (item is IElement element)
{
var targetName = element.Name;
var targetNameExists = await target.IsExists(targetName);
if (transportMode == TransportMode.Merge)
if (transportMode == Command.TransportMode.Merge)
{
for (var i = 0; targetNameExists; i++)
{
targetName = element.Name + (i == 0 ? "_" : $"_{i}");
}
}
else if (transportMode == TransportMode.Skip && targetNameExists)
else if (transportMode == Command.TransportMode.Skip && targetNameExists)
{
continue;
}
var targetPath = target.FullName + Constants.SeparatorChar + targetName;
copy(new AbsolutePath(source.ContentProvider, element.FullName!), new AbsolutePath(target.Provider, targetPath));
_copyOperation?.Invoke(new AbsolutePath(element), AbsolutePath.FromParentAndChildName(target, targetName));
}
}
}
public Task<CanCommandRun> CanRun(PointInTime startPoint)
{
//TODO: implement
return Task.FromResult(CanCommandRun.True);
}
}
}

View File

@@ -1,12 +1,51 @@
using FileTime.Core.Models;
using FileTime.Core.Timeline;
namespace FileTime.Core.Command
{
public class CreateContainerCommand : ICommand
public class CreateContainerCommand : IExecutableCommand
{
public PointInTime SimulateCommand(PointInTime delta)
public AbsolutePath Container { get; }
public string NewContainerName { get; }
public CreateContainerCommand(AbsolutePath container, string newContainerName)
{
throw new NotImplementedException();
Container = container;
NewContainerName = newContainerName;
}
public async Task Execute(TimeRunner timeRunner)
{
var possibleContainer = await Container.Resolve();
if (possibleContainer is IContainer container)
{
await container.CreateContainer(NewContainerName);
await timeRunner.RefreshContainer.InvokeAsync(this, new AbsolutePath(container));
}
//TODO: else
}
public Task<PointInTime> SimulateCommand(PointInTime startPoint)
{
var newDifferences = new List<Difference>()
{
new Difference(DifferenceItemType.Container, DifferenceActionType.Create, new AbsolutePath(Container.ContentProvider, Container.Path + Constants.SeparatorChar + NewContainerName, Container.VirtualContentProvider))
};
return Task.FromResult(startPoint.WithDifferences(newDifferences));
}
public async Task<CanCommandRun> CanRun(PointInTime startPoint)
{
var resolvedContainer = await Container.Resolve();
if (resolvedContainer == null) return CanCommandRun.Forceable;
if (resolvedContainer is not IContainer container
|| await container.IsExists(NewContainerName))
{
return CanCommandRun.False;
}
return CanCommandRun.True;
}
}
}

View File

@@ -1,12 +1,50 @@
using FileTime.Core.Models;
using FileTime.Core.Timeline;
namespace FileTime.Core.Command
{
public class CreateElementCommand : ICommand
public class CreateElementCommand : IExecutableCommand
{
public PointInTime SimulateCommand(PointInTime delta)
public AbsolutePath Container { get; }
public string NewElementName { get; }
public CreateElementCommand(AbsolutePath container, string newElementName)
{
throw new NotImplementedException();
Container = container;
NewElementName = newElementName;
}
public async Task Execute(TimeRunner timeRunner)
{
var possibleContainer = await Container.Resolve();
if (possibleContainer is IContainer container)
{
await container.CreateElement(NewElementName);
await timeRunner.RefreshContainer.InvokeAsync(this, new AbsolutePath(container));
}
}
public Task<PointInTime> SimulateCommand(PointInTime startPoint)
{
var newDifferences = new List<Difference>()
{
new Difference(DifferenceItemType.Element, DifferenceActionType.Create, new AbsolutePath(Container.ContentProvider, Container.Path + Constants.SeparatorChar + NewElementName, Container.VirtualContentProvider))
};
return Task.FromResult(startPoint.WithDifferences(newDifferences));
}
public async Task<CanCommandRun> CanRun(PointInTime startPoint)
{
var resolvedContainer = Container.Resolve();
if (resolvedContainer == null) return CanCommandRun.Forceable;
if (resolvedContainer is not IContainer container
|| await container.IsExists(NewElementName))
{
return CanCommandRun.False;
}
return CanCommandRun.True;
}
}
}

View File

@@ -1,3 +1,4 @@
using FileTime.Core.Extensions;
using FileTime.Core.Models;
using FileTime.Core.Timeline;
@@ -5,37 +6,64 @@ namespace FileTime.Core.Command
{
public class DeleteCommand : IExecutableCommand
{
public IList<IAbsolutePath> ItemsToDelete { get; } = new List<IAbsolutePath>();
public IList<AbsolutePath> ItemsToDelete { get; } = new List<AbsolutePath>();
public PointInTime SimulateCommand(PointInTime delta)
public async Task<PointInTime> SimulateCommand(PointInTime startPoint)
{
throw new NotImplementedException();
var newDifferences = new List<Difference>();
foreach (var itemToDelete in ItemsToDelete)
{
var item = await itemToDelete.Resolve();
newDifferences.Add(new Difference(
item.ToDifferenceItemType(),
DifferenceActionType.Delete,
itemToDelete
));
}
return startPoint.WithDifferences(newDifferences);
}
public async Task Execute()
public async Task Execute(TimeRunner timeRunner)
{
foreach (var item in ItemsToDelete)
{
await DoDelete(await item.ContentProvider.GetByPath(item.Path)!);
await DoDelete((await item.Resolve())!, timeRunner);
}
}
private async Task DoDelete(IItem item)
private async Task DoDelete(IItem item, TimeRunner timeRunner)
{
if (item is IContainer container)
{
foreach (var child in await container.GetItems())
foreach (var child in (await container.GetItems())!)
{
await DoDelete(child);
await DoDelete(child, timeRunner);
await child.Delete();
}
await item.Delete();
await timeRunner.RefreshContainer.InvokeAsync(this, new AbsolutePath(container));
}
else if(item is IElement element)
else if (item is IElement element)
{
await element.Delete();
}
}
public async Task<CanCommandRun> CanRun(PointInTime startPoint)
{
var result = CanCommandRun.True;
foreach (var itemPath in ItemsToDelete)
{
var resolvedItem = await itemPath.Resolve();
if (!(resolvedItem?.CanDelete ?? true))
{
result = CanCommandRun.Forceable;
}
}
return result;
}
}
}

View File

@@ -4,6 +4,7 @@ namespace FileTime.Core.Command
{
public interface ICommand
{
PointInTime SimulateCommand(PointInTime moment);
Task<CanCommandRun> CanRun(PointInTime startPoint);
Task<PointInTime> SimulateCommand(PointInTime startPoint);
}
}

View File

@@ -1,8 +1,10 @@
using FileTime.Core.Timeline;
namespace FileTime.Core.Command
{
public interface ICommandHandler
{
bool CanHandle(object command);
void Execute(object command);
Task ExecuteAsync(object command, TimeRunner timeRunner);
}
}

View File

@@ -1,7 +1,9 @@
using FileTime.Core.Timeline;
namespace FileTime.Core.Command
{
public interface IExecutableCommand : ICommand
{
Task Execute();
Task Execute(TimeRunner timeRunner);
}
}

View File

@@ -4,8 +4,8 @@ namespace FileTime.Core.Command
{
public interface ITransportationCommand : ICommand
{
IList<IAbsolutePath> Sources { get; }
IContainer Target { get; set;}
TransportMode TransportMode { get; set; }
IList<AbsolutePath>? Sources { get; }
IContainer? Target { get; set;}
TransportMode? TransportMode { get; set; }
}
}

View File

@@ -5,12 +5,17 @@ namespace FileTime.Core.Command
{
public class MoveCommand : ITransportationCommand
{
public IList<IAbsolutePath> Sources { get; } = new List<IAbsolutePath>();
public IList<AbsolutePath>? Sources { get; } = new List<AbsolutePath>();
public IContainer? Target { get; set; }
public TransportMode TransportMode { get; set; } = TransportMode.Merge;
public TransportMode? TransportMode { get; set; } = Command.TransportMode.Merge;
public PointInTime SimulateCommand(PointInTime delta)
public Task<CanCommandRun> CanRun(PointInTime startPoint)
{
throw new NotImplementedException();
}
public Task<PointInTime> SimulateCommand(PointInTime startPoint)
{
throw new NotImplementedException();
}

View File

@@ -0,0 +1,37 @@
using FileTime.Core.Models;
using FileTime.Core.Timeline;
namespace FileTime.Core.Command
{
public class RenameCommand : IExecutableCommand
{
public AbsolutePath Source { get; }
public string Target { get; }
public RenameCommand(AbsolutePath source, string target)
{
Source = source;
Target = target;
}
public async Task Execute(TimeRunner timeRunner)
{
var itemToRename = await Source.Resolve();
if (itemToRename != null)
{
await itemToRename.Rename(Target);
timeRunner.RefreshContainer?.InvokeAsync(this, new AbsolutePath(itemToRename.GetParent()!));
}
}
public Task<PointInTime> SimulateCommand(PointInTime startPoint)
{
throw new NotImplementedException();
}
public Task<CanCommandRun> CanRun(PointInTime startPoint)
{
throw new NotImplementedException();
}
}
}

View File

@@ -7,7 +7,8 @@ namespace FileTime.Core.Components
{
private IItem? _currentSelectedItem;
private IContainer _currentLocation;
private string? _lastPath;
public int CurrentSelectedIndex { get; private set; }
public AsyncEventHandler CurrentLocationChanged = new();
@@ -33,10 +34,10 @@ namespace FileTime.Core.Components
}
_currentLocation = value;
await CurrentLocationChanged?.InvokeAsync(this, AsyncEventArgs.Empty);
await CurrentLocationChanged.InvokeAsync(this, AsyncEventArgs.Empty);
var currentLocationItems = (await (await GetCurrentLocation()).GetItems())!;
await SetCurrentSelectedItem(currentLocationItems.Count > 0 ? currentLocationItems[0] : null);
await SetCurrentSelectedItem(await GetItemByLastPath() ?? (currentLocationItems.Count > 0 ? currentLocationItems[0] : null));
_currentLocation.Refreshed.Add(HandleCurrentLocationRefresh);
}
}
@@ -58,14 +59,71 @@ namespace FileTime.Core.Components
}
_currentSelectedItem = itemToSelect;
_lastPath = GetCommonPath(_lastPath, itemToSelect?.FullName);
CurrentSelectedIndex = await GetItemIndex(itemToSelect);
await CurrentSelectedItemChanged?.InvokeAsync(this, AsyncEventArgs.Empty);
await CurrentSelectedItemChanged.InvokeAsync(this, AsyncEventArgs.Empty);
}
}
public async Task<IItem?> GetItemByLastPath(IContainer? container = null)
{
container ??= _currentLocation;
var containerFullName = container.FullName;
if (_lastPath == null
|| !container.IsLoaded
|| (containerFullName != null && !_lastPath.StartsWith(containerFullName))
)
{
return null;
}
var itemNameToSelect = _lastPath
.Split(Constants.SeparatorChar)
.Skip(
containerFullName == null
? 0
: containerFullName
.Split(Constants.SeparatorChar)
.Count())
.FirstOrDefault();
return (await container.GetItems())?.FirstOrDefault(i => i.Name == itemNameToSelect);
}
private string GetCommonPath(string? oldPath, string? newPath)
{
var oldPathParts = oldPath?.Split(Constants.SeparatorChar) ?? new string[0];
var newPathParts = newPath?.Split(Constants.SeparatorChar) ?? new string[0];
var commonPathParts = new List<string>();
var max = oldPathParts.Length > newPathParts.Length ? oldPathParts.Length : newPathParts.Length;
for (var i = 0; i < max; i++)
{
if (newPathParts.Length <= i)
{
commonPathParts.AddRange(oldPathParts.Skip(i));
break;
}
else if (oldPathParts.Length <= i || oldPathParts[i] != newPathParts[i])
{
commonPathParts.AddRange(newPathParts.Skip(i));
break;
}
else if (oldPathParts[i] == newPathParts[i])
{
commonPathParts.Add(oldPathParts[i]);
}
}
return string.Join(Constants.SeparatorChar, commonPathParts);
}
private async Task HandleCurrentLocationRefresh(object? sender, AsyncEventArgs e)
{
var currentSelectedName = (await GetCurrentSelectedItem())?.FullName;
var currentSelectedName = (await GetCurrentSelectedItem())?.FullName ?? (await GetItemByLastPath()).FullName;
var currentLocationItems = (await (await GetCurrentLocation()).GetItems())!;
if (currentSelectedName != null)
{

View File

@@ -0,0 +1,15 @@
using FileTime.Core.Models;
using FileTime.Core.Timeline;
namespace FileTime.Core.Extensions
{
public static class TimelineExtensions
{
public static DifferenceItemType ToDifferenceItemType(this IItem? item)
{
if (item is IContainer) return DifferenceItemType.Container;
else if (item is IElement) return DifferenceItemType.Element;
else return DifferenceItemType.Unknown;
}
}
}

View File

@@ -4,11 +4,13 @@ namespace FileTime.Core.Interactions
{
public string Text { get; }
public InputType InputType { get; }
public string? DefaultValue { get; }
public InputElement(string text, InputType inputType)
public InputElement(string text, InputType inputType, string? defaultValue = null)
{
Text = text;
InputType = inputType;
DefaultValue = defaultValue;
}
}
}

View File

@@ -3,6 +3,7 @@ namespace FileTime.Core.Interactions
public enum InputType
{
Text,
Password
Password,
Bool
}
}

View File

@@ -1,17 +1,97 @@
using FileTime.Core.Providers;
using FileTime.Core.Timeline;
namespace FileTime.Core.Models
{
public class AbsolutePath : IAbsolutePath
public sealed class AbsolutePath
{
public IContentProvider ContentProvider { get; }
public IContentProvider? VirtualContentProvider { get; }
public string Path { get; }
public AbsolutePath(IContentProvider contentProvider, string path)
public AbsolutePath(AbsolutePath from)
{
ContentProvider = from.ContentProvider;
Path = from.Path;
VirtualContentProvider = from.VirtualContentProvider;
}
public AbsolutePath(IContentProvider contentProvider, string path, IContentProvider? virtualContentProvider)
{
ContentProvider = contentProvider;
Path = path;
VirtualContentProvider = virtualContentProvider;
}
public AbsolutePath(IItem item)
{
if (item is TimeContainer timeContainer)
{
ContentProvider = timeContainer.Provider;
VirtualContentProvider = timeContainer.VirtualProvider;
Path = timeContainer.FullName!;
}
else if (item is TimeElement timeElement)
{
ContentProvider = timeElement.Provider;
VirtualContentProvider = timeElement.VirtualProvider;
Path = timeElement.FullName!;
}
else
{
ContentProvider = item.Provider;
Path = item.FullName!;
}
}
public static AbsolutePath FromParentAndChildName(IContainer parent, string childName)
{
IContentProvider? contentProvider;
IContentProvider? virtualContentProvider;
string? path;
if (parent is TimeContainer timeContainer)
{
contentProvider = timeContainer.Provider;
virtualContentProvider = timeContainer.VirtualProvider;
path = timeContainer.FullName! + Constants.SeparatorChar + childName;
}
else
{
contentProvider = parent.Provider;
path = parent.FullName! + Constants.SeparatorChar + childName;
virtualContentProvider = null;
}
return new AbsolutePath(contentProvider, path, virtualContentProvider);
}
public bool IsEqual(AbsolutePath path)
{
//TODO: sure??
return path.ContentProvider == ContentProvider && path.Path == Path;
}
public async Task<IItem?> Resolve()
{
var result = VirtualContentProvider != null && (await VirtualContentProvider.IsExists(Path))
? await VirtualContentProvider.GetByPath(Path)
: null;
result ??= await ContentProvider.GetByPath(Path);
return result;
}
public string GetParent()
{
var pathParts = Path.Split(Constants.SeparatorChar);
return string.Join(Constants.SeparatorChar, pathParts);
}
public AbsolutePath GetParentAsAbsolutePath() => new(ContentProvider, GetParent(), VirtualContentProvider);
public string GetName() => Path.Split(Constants.SeparatorChar).Last();
}
}

View File

@@ -1,10 +0,0 @@
using FileTime.Core.Providers;
namespace FileTime.Core.Models
{
public interface IAbsolutePath
{
IContentProvider ContentProvider { get; }
string Path { get; }
}
}

View File

@@ -9,7 +9,6 @@ namespace FileTime.Core.Models
Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default);
Task Refresh();
IContainer? GetParent();
Task<IItem?> GetByPath(string path);
Task<IContainer> CreateContainer(string name);
Task<IElement> CreateElement(string name);
@@ -18,6 +17,8 @@ namespace FileTime.Core.Models
Task<IContainer> Clone();
bool IsLoaded { get; }
AsyncEventHandler Refreshed { get; }
}
}

View File

@@ -7,7 +7,11 @@ namespace FileTime.Core.Models
string Name { get; }
string? FullName { get; }
bool IsHidden { get; }
bool CanDelete { get; }
bool CanRename { get; }
IContentProvider Provider { get; }
Task Delete();
Task Rename(string newName);
IContainer? GetParent();
}
}

View File

@@ -24,6 +24,9 @@ namespace FileTime.Core.Models
public string? FullName => BaseContainer.FullName;
public bool IsHidden => BaseContainer.IsHidden;
public bool IsLoaded => BaseContainer.IsLoaded;
public bool CanDelete => BaseContainer.CanDelete;
public bool CanRename => BaseContainer.CanRename;
public IContentProvider Provider => BaseContainer.Provider;
@@ -159,5 +162,7 @@ namespace FileTime.Core.Models
VirtualContainerName
);
}
public async Task Rename(string newName) => await BaseContainer.Rename(newName);
}
}

View File

@@ -1,4 +1,5 @@
using System;
using AsyncEvent;
using FileTime.Core.Models;
@@ -16,8 +17,11 @@ namespace FileTime.Core.Providers
public string? FullName => null;
public bool IsHidden => false;
public bool IsLoaded => true;
public IContentProvider Provider => null;
public bool CanDelete => false;
public bool CanRename => false;
public AsyncEventHandler Refreshed { get; } = new();
@@ -52,5 +56,7 @@ namespace FileTime.Core.Providers
public Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default) => Task.FromResult(_elements);
public Task<IContainer> Clone() => Task.FromResult((IContainer)this);
public Task Rename(string newName) => throw new NotSupportedException();
}
}

View File

@@ -0,0 +1,27 @@
using FileTime.Core.Command;
namespace FileTime.Core.Timeline
{
public class CommandTimeState
{
public ICommand Command { get; }
public CanCommandRun CanRun { get; private set; } = CanCommandRun.False;
public bool ForceRun { get; set; }
public TimeProvider? TimeProvider { get; private set; }
public CommandTimeState(ICommand command, PointInTime? startTime)
{
Command = command;
UpdateState(startTime).Wait();
}
public async Task UpdateState(PointInTime? startPoint)
{
CanRun = startPoint == null ? CanCommandRun.False : await Command.CanRun(startPoint);
if (startPoint != null)
{
TimeProvider = startPoint.Provider as TimeProvider;
}
}
}
}

View File

@@ -1,7 +0,0 @@
namespace FileTime.Core.Timeline
{
public class ContainerSnapshot
{
}
}

View File

@@ -0,0 +1,31 @@
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.Core.Timeline
{
public class Difference
{
public DifferenceItemType Type { get; }
public string Name { get; }
public AbsolutePath AbsolutePath { get; }
public DifferenceActionType Action { get; }
public Difference(DifferenceItemType type, DifferenceActionType action, AbsolutePath absolutePath)
{
Type = type;
AbsolutePath = absolutePath;
Action = action;
Name = absolutePath.GetName();
}
public Difference WithVirtualContentProvider(IContentProvider? virtualContentProvider)
{
return new Difference(
Type,
Action,
new AbsolutePath(AbsolutePath.ContentProvider, AbsolutePath.Path, virtualContentProvider)
);
}
}
}

View File

@@ -0,0 +1,8 @@
namespace FileTime.Core.Timeline
{
public enum DifferenceActionType
{
Create,
Delete
}
}

View File

@@ -0,0 +1,9 @@
namespace FileTime.Core.Timeline
{
public enum DifferenceItemType
{
Container,
Element,
Unknown
}
}

View File

@@ -0,0 +1,89 @@
using FileTime.Core.Command;
namespace FileTime.Core.Timeline
{
public class ParallelCommands
{
private static ushort _idCounter;
public List<CommandTimeState> _commands;
public ushort Id { get; }
public IReadOnlyList<CommandTimeState> Commands { get; }
public PointInTime? Result { get; private set; }
public ParallelCommands(PointInTime? result)
: this(new List<CommandTimeState>(), result) { }
private ParallelCommands(List<CommandTimeState> commands, PointInTime? result)
{
Id = _idCounter++;
_commands = commands;
Commands = _commands.AsReadOnly();
Result = result;
}
public static async Task<ParallelCommands> Create(PointInTime? startTime, IEnumerable<ICommand> commands)
{
var commandStates = new List<CommandTimeState>();
var currentTime = startTime;
foreach (var command in commands)
{
CommandTimeState commandTimeState = new(command, currentTime);
if (currentTime != null)
{
var canRun = await command.CanRun(currentTime);
if (canRun == CanCommandRun.True)
{
currentTime = await command.SimulateCommand(currentTime);
}
else
{
currentTime = null;
}
}
commandStates.Add(commandTimeState);
}
return new ParallelCommands(commandStates, currentTime);
}
public async Task AddCommand(ICommand command)
{
_commands.Add(new CommandTimeState(command, Result));
if (Result != null)
{
Result = await command.SimulateCommand(Result);
}
}
public async Task<PointInTime?> RefreshResult(PointInTime? startPoint)
{
var result = startPoint;
foreach (var command in _commands)
{
await command.UpdateState(result);
if (result != null)
{
var canRun = await command.Command.CanRun(result);
if (canRun == CanCommandRun.True || (canRun == CanCommandRun.Forceable && command.ForceRun))
{
result = await command.Command.SimulateCommand(result);
}
else
{
result = null;
}
}
}
Result = result;
return Result;
}
public void RemoveAt(int number) => _commands.RemoveAt(number);
internal void Remove(CommandTimeState command) => _commands.Remove(command);
}
}

View File

@@ -1,12 +1,40 @@
using System.Collections.ObjectModel;
using FileTime.Core.Providers;
namespace FileTime.Core.Timeline
{
public class PointInTime
public sealed class PointInTime
{
private readonly Dictionary<IContentProvider, RootSnapshot> snapshots = new();
private readonly List<Difference> _differences;
public IReadOnlyDictionary<IContentProvider, RootSnapshot> Snapshots => new Lazy<IReadOnlyDictionary<IContentProvider, RootSnapshot>>(() => new ReadOnlyDictionary<IContentProvider, RootSnapshot>(snapshots)).Value;
public IReadOnlyList<Difference> Differences { get; }
public IContentProvider? Provider { get; }
private PointInTime() : this(new List<Difference>(), null) { }
private PointInTime(IEnumerable<Difference> differences, IContentProvider? provider)
{
_differences = new List<Difference>(differences);
Differences = _differences.AsReadOnly();
Provider = provider;
}
private PointInTime(PointInTime previous, IEnumerable<Difference> differences, IContentProvider provider)
: this(MergeDifferences(previous.Differences, differences, provider), provider) { }
public PointInTime WithDifferences(IEnumerable<Difference> differences) => new(this, differences, new TimeProvider(this));
private static List<Difference> MergeDifferences(IEnumerable<Difference> previouses, IEnumerable<Difference> differences, IContentProvider virtualProvider)
{
var merged = new List<Difference>();
merged.AddRange(previouses.Select(p => p.WithVirtualContentProvider(virtualProvider)));
merged.AddRange(differences.Select(d => d.WithVirtualContentProvider(virtualProvider)));
return merged;
}
public static PointInTime CreateEmpty(IContentProvider? parentProvder = null) =>
parentProvder == null ? new PointInTime() : new PointInTime(new List<Difference>(), parentProvder);
}
}

View File

@@ -0,0 +1,18 @@
using FileTime.Core.Command;
namespace FileTime.Core.Timeline
{
public class ReadOnlyCommandTimeState
{
public CanCommandRun CanRun { get; }
public bool ForceRun { get; }
public ICommand Command { get; }
public ReadOnlyCommandTimeState(CommandTimeState commandTimeState)
{
CanRun = commandTimeState.CanRun;
ForceRun = commandTimeState.ForceRun;
Command = commandTimeState.Command;
}
}
}

View File

@@ -0,0 +1,11 @@
namespace FileTime.Core.Timeline
{
public class ReadOnlyParallelCommands
{
public IReadOnlyList<ReadOnlyCommandTimeState> Commands { get; }
public ReadOnlyParallelCommands(ParallelCommands parallelCommands)
{
Commands = parallelCommands.Commands.Select(c => new ReadOnlyCommandTimeState(c)).ToList().AsReadOnly();
}
}
}

View File

@@ -1,7 +0,0 @@
namespace FileTime.Core.Timeline
{
public class RootSnapshot
{
}
}

View File

@@ -0,0 +1,121 @@
using AsyncEvent;
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.Core.Timeline
{
public class TimeContainer : IContainer
{
private readonly IContainer? _parent;
private readonly PointInTime _pointInTime;
public bool IsLoaded => true;
public AsyncEventHandler Refreshed { get; } = new AsyncEventHandler();
public string Name { get; }
public string? FullName { get; }
public bool IsHidden => false;
public bool CanDelete => true;
public bool CanRename => true;
public IContentProvider Provider { get; }
public IContentProvider VirtualProvider { get; }
public TimeContainer(string name, IContainer parent, IContentProvider contentProvider, IContentProvider virtualContentProvider, PointInTime pointInTime)
{
_parent = parent;
_pointInTime = pointInTime;
Name = name;
Provider = contentProvider;
VirtualProvider = virtualContentProvider;
FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name;
}
public async Task<IContainer> Clone() => new TimeContainer(Name, await _parent!.Clone(), Provider, VirtualProvider, _pointInTime);
public Task<IContainer> CreateContainer(string name) => Task.FromResult((IContainer)new TimeContainer(name, this, Provider, VirtualProvider, _pointInTime));
public Task<IElement> CreateElement(string name) => Task.FromResult((IElement)new TimeElement(name, this, Provider, VirtualProvider));
public Task Delete() => Task.CompletedTask;
public async Task<IItem?> GetByPath(string path)
{
var paths = path.Split(Constants.SeparatorChar);
var item = (await GetItems())!.FirstOrDefault(i => i.Name == paths[0]);
if (paths.Length == 1)
{
return item;
}
if (item is IContainer container)
{
return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)));
}
return null;
}
public Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default) =>
Task.FromResult(
(IReadOnlyList<IContainer>?)_pointInTime
.Differences
.Where(d =>
d.Type == DifferenceItemType.Container
&& GetParentPath(d.AbsolutePath.Path) == FullName)
.Select(MapContainer)
.ToList()
.AsReadOnly()
);
public Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default) =>
Task.FromResult(
(IReadOnlyList<IElement>?)_pointInTime
.Differences
.Where(d =>
d.Type == DifferenceItemType.Element
&& GetParentPath(d.AbsolutePath.Path) == FullName)
.Select(MapElement)
.ToList()
.AsReadOnly()
);
public async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
{
var containers = (await GetContainers(token))!;
var elements = (await GetElements(token))!;
return containers.Cast<IItem>().Concat(elements).ToList().AsReadOnly();
}
public IContainer? GetParent() => _parent;
public async Task<bool> IsExists(string name) => (await GetItems())?.Any(i => i.Name == name) ?? false;
public async Task Refresh() => await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty);
public Task Rename(string newName) => Task.CompletedTask;
private static string GetParentPath(string path) => string.Join(Constants.SeparatorChar, path.Split(Constants.SeparatorChar).Take(-1));
private IContainer MapContainer(Difference containerDiff)
{
if (containerDiff.Type != DifferenceItemType.Container) throw new ArgumentException($"{nameof(containerDiff)}'s {nameof(Difference.Type)} property is not {DifferenceItemType.Container}.");
return new TimeContainer(containerDiff.Name, this, Provider, containerDiff.AbsolutePath.VirtualContentProvider ?? containerDiff.AbsolutePath.ContentProvider, _pointInTime);
}
private IElement MapElement(Difference elementDiff)
{
if (elementDiff.Type != DifferenceItemType.Container) throw new ArgumentException($"{elementDiff}'s {nameof(Difference.Type)} property is not {DifferenceItemType.Element}.");
return new TimeElement(elementDiff.Name, this, Provider, elementDiff.AbsolutePath.VirtualContentProvider ?? elementDiff.AbsolutePath.ContentProvider);
}
}
}

View File

@@ -0,0 +1,42 @@
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.Core.Timeline
{
public class TimeElement : IElement
{
private readonly IContainer _parent;
public TimeElement(string name, TimeContainer parent, IContentProvider contentProvider, IContentProvider virtualContentProvider)
{
_parent = parent;
Name = name;
FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name;
Provider = contentProvider;
VirtualProvider = virtualContentProvider;
}
public bool IsSpecial => false;
public string Name { get; }
public string? FullName { get; }
public bool IsHidden => false;
public bool CanDelete => true;
public bool CanRename => true;
public IContentProvider Provider { get; }
public IContentProvider VirtualProvider { get; }
public Task Delete() => Task.CompletedTask;
public IContainer? GetParent() => _parent;
public string GetPrimaryAttributeText() => "";
public Task Rename(string newName) => Task.CompletedTask;
}
}

View File

@@ -0,0 +1,89 @@
using AsyncEvent;
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.Core.Timeline
{
public class TimeProvider : IContentProvider
{
public bool IsLoaded => true;
public AsyncEventHandler Refreshed { get; } = new();
public string Name => "time";
public string? FullName => null;
public bool IsHidden => false;
public bool CanDelete => false;
public bool CanRename => false;
public IContentProvider Provider => this;
private readonly PointInTime _pointInTime;
public TimeProvider(PointInTime pointInTime)
{
_pointInTime = pointInTime;
}
public bool CanHandlePath(string path)
{
throw new NotImplementedException();
}
public Task<IContainer> Clone() => Task.FromResult((IContainer)this);
public Task<IContainer> CreateContainer(string name)
{
throw new NotImplementedException();
}
public Task<IElement> CreateElement(string name)
{
throw new NotImplementedException();
}
public Task Delete() => throw new NotSupportedException();
public Task<IItem?> GetByPath(string path)
{
throw new NotImplementedException();
}
public Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default)
{
throw new NotImplementedException();
}
public Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default)
{
throw new NotImplementedException();
}
public Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
{
throw new NotImplementedException();
}
public IContainer? GetParent() => null;
public Task<IReadOnlyList<IContainer>> GetRootContainers(CancellationToken token = default)
{
throw new NotImplementedException();
}
public Task<bool> IsExists(string name)
{
throw new NotImplementedException();
}
public Task Refresh() => Task.CompletedTask;
public Task Rename(string newName) => throw new NotSupportedException();
public void SetParent(IContainer container) { }
}
}

View File

@@ -0,0 +1,217 @@
using AsyncEvent;
using FileTime.Core.Command;
using FileTime.Core.Models;
namespace FileTime.Core.Timeline
{
public class TimeRunner
{
private readonly CommandExecutor _commandExecutor;
private readonly List<ParallelCommands> _commandsToRun = new();
private readonly object _guard = new();
private bool _resourceIsInUse;
private readonly List<Thread> _commandRunners = new();
private bool _enableRunning = true;
public bool EnableRunning
{
get
{
bool result = true;
RunWithLock(() => result = _enableRunning);
return result;
}
set
{
RunWithLock(() => _enableRunning = value);
}
}
public IReadOnlyList<ReadOnlyParallelCommands> ParallelCommands { get; private set; } = new List<ReadOnlyParallelCommands>().AsReadOnly();
public AsyncEventHandler<AbsolutePath> RefreshContainer { get; } = new AsyncEventHandler<AbsolutePath>();
public event EventHandler? CommandsChanged;
public TimeRunner(CommandExecutor commandExecutor)
{
_commandExecutor = commandExecutor;
}
public async Task AddCommand(ICommand command, ParallelCommands? batch = null, bool toNewBatch = false)
{
await RunWithLockAsync(async () =>
{
ParallelCommands batchToAdd;
if (_commandsToRun.Count == 0)
{
batchToAdd = new ParallelCommands(PointInTime.CreateEmpty());
_commandsToRun.Add(batchToAdd);
}
else if (toNewBatch)
{
batchToAdd = new ParallelCommands(_commandsToRun.Last().Result);
_commandsToRun.Add(batchToAdd);
}
else if (batch != null && _commandsToRun.Contains(batch))
{
batchToAdd = batch;
}
else
{
batchToAdd = _commandsToRun[0];
}
await batchToAdd.AddCommand(command);
await RefreshCommands();
if (_commandRunners.Count == 0)
{
StartCommandRunner();
}
});
UpdateReadOnlyCommands();
}
public async Task TryStartCommandRunner()
{
await RunWithLockAsync(() =>
{
if (_commandRunners.Count == 0 && _commandsToRun.Count > 0)
{
StartCommandRunner();
}
});
}
private void StartCommandRunner()
{
if (_enableRunning)
{
RunCommands();
}
}
private void RunCommands()
{
if (_commandsToRun.Count > 0)
{
foreach (var command in _commandsToRun[0].Commands)
{
if (command.CanRun == CanCommandRun.True || (command.CanRun == CanCommandRun.Forceable && command.ForceRun))
{
var thread = new Thread(new ParameterizedThreadStart(RunCommand));
thread.Start(command);
}
else
{
break;
}
}
}
}
private void RunCommand(object? arg)
{
CommandTimeState? commandToRun = null;
try
{
if (arg is CommandTimeState commandToRun2)
{
commandToRun = commandToRun2;
_commandExecutor.ExecuteCommandAsync(commandToRun.Command, this).Wait();
}
}
finally
{
DisposeCommandThread(Thread.CurrentThread, commandToRun).Wait();
}
}
private async Task DisposeCommandThread(Thread thread, CommandTimeState? command)
{
await RunWithLockAsync(() =>
{
if (command != null)
{
_commandsToRun[0].Remove(command);
}
_commandRunners.Remove(thread);
});
await UpdateReadOnlyCommands();
await TryStartCommandRunner();
}
public async Task Refresh()
{
await RunWithLockAsync(async () =>
{
await RefreshCommands(PointInTime.CreateEmpty());
});
await UpdateReadOnlyCommands();
}
private async Task RefreshCommands(PointInTime? fullStartTime = null)
{
var curretnTime = fullStartTime ?? _commandsToRun[0].Result;
var startIndex = fullStartTime == null ? 1 : 0;
for (var i = startIndex; i < _commandsToRun.Count; i++)
{
curretnTime = await _commandsToRun[i].RefreshResult(curretnTime);
}
}
private async Task UpdateReadOnlyCommands()
{
await RunWithLockAsync(() =>
{
ParallelCommands = _commandsToRun.ConvertAll(c => new ReadOnlyParallelCommands(c)).AsReadOnly();
});
CommandsChanged?.Invoke(this, EventArgs.Empty);
}
private async Task RunWithLockAsync(Action action)
{
await RunWithLockAsync(() => { action(); return Task.CompletedTask; });
}
private async Task RunWithLockAsync(Func<Task> func)
{
while (true)
{
lock (_guard)
{
if (!_resourceIsInUse)
{
_resourceIsInUse = true;
break;
}
}
await Task.Delay(1);
}
try
{
await func();
}
finally
{
lock (_guard)
{
_resourceIsInUse = false;
}
}
}
private void RunWithLock(Action action) => RunWithLockAsync(action).Wait();
}
}