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

@@ -1,35 +1,36 @@
using FileTime.Core.Command;
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.App.Core.Clipboard
{
public class Clipboard : IClipboard
{
private readonly List<ClipboardItem> _content;
public IReadOnlyList<ClipboardItem> Content { get; }
private readonly List<AbsolutePath> _content;
public IReadOnlyList<AbsolutePath> Content { get; }
public Type? CommandType { get; private set; }
public Clipboard()
{
_content = new List<ClipboardItem>();
_content = new List<AbsolutePath>();
Content = _content.AsReadOnly();
}
public void AddContent(IContentProvider contentProvider, string path)
public void AddContent(AbsolutePath absolutePath)
{
foreach (var content in _content)
{
if (content.ContentProvider == contentProvider && content.Path == path) return;
if (content.IsEqual(absolutePath)) return;
}
_content.Add(new ClipboardItem(contentProvider, path));
_content.Add(new AbsolutePath(absolutePath));
}
public void RemoveContent(IContentProvider contentProvider, string path)
public void RemoveContent(AbsolutePath absolutePath)
{
for (var i = 0; i < _content.Count; i++)
{
if (_content[i].ContentProvider == contentProvider && _content[i].Path == path)
if (_content[i].IsEqual(absolutePath))
{
_content.RemoveAt(i--);
}

View File

@@ -1,17 +0,0 @@
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.App.Core.Clipboard
{
public class ClipboardItem : IAbsolutePath
{
public IContentProvider ContentProvider { get; }
public string Path { get; }
public ClipboardItem(IContentProvider contentProvider, string path)
{
ContentProvider = contentProvider;
Path = path;
}
}
}

View File

@@ -1,16 +1,16 @@
using FileTime.Core.Command;
using FileTime.Core.Providers;
using FileTime.Core.Models;
namespace FileTime.App.Core.Clipboard
{
public interface IClipboard
{
IReadOnlyList<ClipboardItem> Content { get; }
IReadOnlyList<AbsolutePath> Content { get; }
Type? CommandType { get; }
void AddContent(IContentProvider contentProvider, string path);
void AddContent(AbsolutePath absolutePath);
void Clear();
void RemoveContent(IContentProvider contentProvider, string path);
void RemoveContent(AbsolutePath absolutePath);
void SetCommand<T>() where T : ITransportationCommand;
}
}

View File

@@ -4,26 +4,32 @@ namespace FileTime.App.Core.Command
{
CloseTab,
Copy,
Cut,
GoUp,
MoveCursorDown,
MoveCursorUp,
Open,
Paste,
Select,
ToggleHidden,
CreateContainer,
CreateElement,
MoveCursorUpPage,
Cut,
EnterRapidTravel,
GoToHome,
GoToProvider,
GoToRoot,
GoUp,
Delete,
MoveCursorDown,
MoveCursorDownPage,
MoveToTop,
MoveCursorUp,
MoveCursorUpPage,
MoveToBottom,
MoveToFirst,
MoveToLast,
GoToRoot,
GoToProvider,
GoToHome,
EnterRapidTravel,
MoveToTop,
Open,
OpenOrRun,
PasteMerge,
PasteOverwrite,
PasteSkip,
Select,
ToggleHidden,
Rename,
Dummy,
Refresh,
}
}

View File

@@ -1,17 +0,0 @@
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.App.Core.Tab
{
public class TabItem : IAbsolutePath
{
public IContentProvider ContentProvider { get; }
public string Path { get; }
public TabItem(IContentProvider contentProvider, string path)
{
ContentProvider = contentProvider;
Path = path;
}
}
}

View File

@@ -1,77 +1,108 @@
using System.Collections.ObjectModel;
using AsyncEvent;
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.App.Core.Tab
{
public class TabState
{
private readonly Dictionary<IContainer, List<TabItem>> _selectedItems;
private readonly Dictionary<IContainer, IReadOnlyList<TabItem>> _selectedItemsReadOnly;
public IReadOnlyDictionary<IContainer, IReadOnlyList<TabItem>> SelectedItems { get; }
private readonly Dictionary<IContainer, List<AbsolutePath>> _markedItems;
private readonly Dictionary<IContainer, IReadOnlyList<AbsolutePath>> _markedItemsReadOnly;
public IReadOnlyDictionary<IContainer, IReadOnlyList<AbsolutePath>> MarkedItems { get; }
public FileTime.Core.Components.Tab Tab { get; }
public AsyncEventHandler<TabState, AbsolutePath> ItemMarked { get; } = new();
public AsyncEventHandler<TabState, AbsolutePath> ItemUnmarked { get; } = new();
public TabState(FileTime.Core.Components.Tab pane)
{
Tab = pane;
_selectedItems = new Dictionary<IContainer, List<TabItem>>();
_selectedItemsReadOnly = new Dictionary<IContainer, IReadOnlyList<TabItem>>();
SelectedItems = new ReadOnlyDictionary<IContainer, IReadOnlyList<TabItem>>(_selectedItemsReadOnly);
_markedItems = new Dictionary<IContainer, List<AbsolutePath>>();
_markedItemsReadOnly = new Dictionary<IContainer, IReadOnlyList<AbsolutePath>>();
MarkedItems = new ReadOnlyDictionary<IContainer, IReadOnlyList<AbsolutePath>>(_markedItemsReadOnly);
}
public void AddSelectedItem(IContentProvider contentProvider, IContainer container, string path)
public async Task AddMarkedItem(IContainer container, AbsolutePath path)
{
if (!_selectedItems.ContainsKey(container))
if (!_markedItems.ContainsKey(container))
{
var val = new List<TabItem>();
_selectedItems.Add(container, val);
_selectedItemsReadOnly.Add(container, val.AsReadOnly());
var val = new List<AbsolutePath>();
_markedItems.Add(container, val);
_markedItemsReadOnly.Add(container, val.AsReadOnly());
}
foreach (var content in _selectedItems[container])
foreach (var content in _markedItems[container])
{
if (content.ContentProvider == contentProvider && content.Path == path) return;
if (content.IsEqual(path)) return;
}
_selectedItems[container].Add(new TabItem(contentProvider, path));
var tabItem = new AbsolutePath(path);
_markedItems[container].Add(tabItem);
await ItemMarked.InvokeAsync(this, tabItem);
}
public void RemoveSelectedItem(IContentProvider contentProvider, IContainer container, string path)
public async Task RemoveMarkedItem(IContainer container, AbsolutePath path)
{
if (_selectedItems.ContainsKey(container))
if (_markedItems.ContainsKey(container))
{
var selectedItems = _selectedItems[container];
for (var i = 0; i < selectedItems.Count; i++)
var markedItems = _markedItems[container];
for (var i = 0; i < markedItems.Count; i++)
{
if (selectedItems[i].ContentProvider == contentProvider && selectedItems[i].Path == path)
if (markedItems[i].IsEqual(path))
{
selectedItems.RemoveAt(i--);
await ItemUnmarked.InvokeAsync(this, markedItems[i]);
markedItems.RemoveAt(i--);
}
}
}
}
public bool ContainsSelectedItem(IContentProvider contentProvider, IContainer container, string path)
public bool ContainsMarkedItem(IContainer container, AbsolutePath path)
{
if (!_selectedItems.ContainsKey(container)) return false;
if (!_markedItems.ContainsKey(container)) return false;
foreach (var content in _selectedItems[container])
foreach (var content in _markedItems[container])
{
if (content.ContentProvider == contentProvider && content.Path == path) return true;
if (content.Equals(path)) return true;
}
return false;
}
public async Task<IReadOnlyList<TabItem>> GetCurrentSelectedItems()
public async Task<IReadOnlyList<AbsolutePath>> GetCurrentMarkedItems()
{
var currentLocation = await Tab.GetCurrentLocation();
return GetCurrentMarkedItems(await Tab.GetCurrentLocation());
}
return SelectedItems.ContainsKey(currentLocation)
? SelectedItems[currentLocation]
: new List<TabItem>().AsReadOnly();
public IReadOnlyList<AbsolutePath> GetCurrentMarkedItems(IContainer container)
{
return MarkedItems.ContainsKey(container)
? MarkedItems[container]
: new List<AbsolutePath>().AsReadOnly();
}
public async Task MakrCurrentItem()
{
var currentLocation = await Tab!.GetCurrentLocation();
if (currentLocation != null)
{
var currentSelectedItem = await Tab.GetCurrentSelectedItem()!;
if (currentSelectedItem != null)
{
if (ContainsMarkedItem(currentLocation, new AbsolutePath(currentSelectedItem)))
{
await RemoveMarkedItem(currentLocation, new AbsolutePath(currentSelectedItem));
}
else
{
await AddMarkedItem(currentLocation, new AbsolutePath(currentSelectedItem));
}
}
await Tab.SelectNextItem();
}
}
}
}

View File

@@ -2,6 +2,7 @@ using FileTime.App.Core.Clipboard;
using FileTime.Core.Command;
using FileTime.Core.Providers;
using FileTime.Core.StateManagement;
using FileTime.Core.Timeline;
using FileTime.Providers.Local;
using FileTime.Providers.Smb;
using Microsoft.Extensions.DependencyInjection;
@@ -21,7 +22,8 @@ namespace FileTime.App.Core
.AddSingleton<IContentProvider, LocalContentProvider>(sp => sp.GetService<LocalContentProvider>() ?? throw new Exception($"No {nameof(LocalContentProvider)} instance found"))
.AddSingleton<IContentProvider, SmbContentProvider>()
.AddSingleton<ElementCreationStates>()
.AddSingleton<CommandExecutor>();
.AddSingleton<CommandExecutor>()
.AddSingleton<TimeRunner>();
}
}
}

View File

@@ -1,4 +1,3 @@
using FileTime.ConsoleUI.App.UI.Color;
using FileTime.Core.Command;
using FileTime.Core.Extensions;
using FileTime.Core.Models;
@@ -28,8 +27,8 @@ namespace FileTime.ConsoleUI.App
private async Task GoUp() => await _selectedTab!.GoUp();
private async Task Open() => await _selectedTab!.Open();
private async Task MoveCursorUpPage() => await _selectedTab!.SelectPreviousItem(_renderers[_selectedTab].PageSize);
private async Task MoveCursorDownPage() => await _selectedTab!.SelectNextItem(_renderers[_selectedTab].PageSize);
private async Task MoveCursorUpPage() => await _selectedTab!.SelectPreviousItem(UI.Render.PageSize);
private async Task MoveCursorDownPage() => await _selectedTab!.SelectNextItem(UI.Render.PageSize);
private async Task MoveCursorToTop() => await _selectedTab!.SelectFirstItem();
private async Task MoveCursorToBottom() => await _selectedTab!.SelectLastItem();
@@ -38,22 +37,7 @@ namespace FileTime.ConsoleUI.App
const string hiddenFilterName = "filter_showhiddenelements";
var currentLocation = await _selectedTab!.GetCurrentLocation();
/*IContainer containerToOpen = currentLocation;
if (currentLocation is VirtualContainer oldVirtualContainer)
{
containerToOpen = oldVirtualContainer.HasWithName(hiddenFilterName)
? oldVirtualContainer.ExceptWithName(hiddenFilterName)
: GenerateHiddenFilterVirtualContainer(currentLocation);
}
else
{
containerToOpen = GenerateHiddenFilterVirtualContainer(currentLocation);
} */
var containerToOpen = await currentLocation.ToggleVirtualContainerInChain(hiddenFilterName, GenerateHiddenFilterVirtualContainer);
await _selectedTab.OpenContainer(containerToOpen);
static async Task<VirtualContainer> GenerateHiddenFilterVirtualContainer(IContainer container)
@@ -81,20 +65,9 @@ namespace FileTime.ConsoleUI.App
public async Task Select()
{
var currentLocation = await _selectedTab!.GetCurrentLocation();
if (currentLocation != null)
if (_selectedTab != null)
{
var currentSelectedItem = await _selectedTab.GetCurrentSelectedItem()!;
if (_paneStates[_selectedTab].ContainsSelectedItem(currentSelectedItem.Provider, currentLocation, currentSelectedItem.FullName!))
{
_paneStates[_selectedTab].RemoveSelectedItem(currentSelectedItem.Provider, currentLocation, currentSelectedItem.FullName!);
}
else
{
_paneStates[_selectedTab].AddSelectedItem(currentSelectedItem.Provider, currentLocation, currentSelectedItem.FullName!);
}
await _selectedTab.SelectNextItem();
await _tabStates[_selectedTab].MakrCurrentItem();
}
}
@@ -103,18 +76,18 @@ namespace FileTime.ConsoleUI.App
_clipboard.Clear();
_clipboard.SetCommand<CopyCommand>();
var currentSelectedItems = await _paneStates[_selectedTab!].GetCurrentSelectedItems();
var currentSelectedItems = await _tabStates[_selectedTab!].GetCurrentMarkedItems();
if (currentSelectedItems.Count > 0)
{
foreach (var selectedItem in currentSelectedItems)
{
_clipboard.AddContent(selectedItem.ContentProvider, selectedItem.Path);
_clipboard.AddContent(new AbsolutePath(selectedItem));
}
}
else
{
var currentSelectedItem = (await _selectedTab!.GetCurrentSelectedItem())!;
_clipboard.AddContent(currentSelectedItem.Provider, currentSelectedItem.FullName!);
_clipboard.AddContent(new AbsolutePath(currentSelectedItem));
}
}
@@ -157,7 +130,7 @@ namespace FileTime.ConsoleUI.App
? virtualContainer.BaseContainer
: currentLocation;
_commandExecutor.ExecuteCommand(command);
await _timeRunner.AddCommand(command);
_clipboard.Clear();
}
@@ -194,9 +167,9 @@ namespace FileTime.ConsoleUI.App
private async Task HardDelete()
{
IList<IAbsolutePath>? itemsToDelete = null;
IList<AbsolutePath>? itemsToDelete = null;
var currentSelectedItems = await _paneStates[_selectedTab!].GetCurrentSelectedItems();
var currentSelectedItems = (await _tabStates[_selectedTab!].GetCurrentMarkedItems()).Select(p => p.Resolve()).ToList();
var currentSelectedItem = await _selectedTab?.GetCurrentSelectedItem();
if (currentSelectedItems.Count > 0)
{
@@ -212,7 +185,7 @@ namespace FileTime.ConsoleUI.App
if (delete)
{
itemsToDelete = currentSelectedItems.Cast<IAbsolutePath>().ToList();
itemsToDelete = currentSelectedItems.Cast<AbsolutePath>().ToList();
}
}
else if (currentSelectedItem != null)
@@ -225,9 +198,9 @@ namespace FileTime.ConsoleUI.App
if (delete)
{
itemsToDelete = new List<IAbsolutePath>()
itemsToDelete = new List<AbsolutePath>()
{
new AbsolutePath(currentSelectedItem.Provider, currentSelectedItem.FullName!)
new AbsolutePath(currentSelectedItem)
};
}
}
@@ -241,7 +214,7 @@ namespace FileTime.ConsoleUI.App
deleteCommand.ItemsToDelete.Add(itemToDelete);
}
_commandExecutor.ExecuteCommand(deleteCommand);
await _timeRunner.AddCommand(deleteCommand);
_clipboard.Clear();
}

View File

@@ -10,6 +10,7 @@ using FileTime.App.Core.Tab;
using FileTime.ConsoleUI.App.UI.Color;
using FileTime.Core.Command;
using FileTime.App.Core.Command;
using FileTime.Core.Timeline;
namespace FileTime.ConsoleUI.App
{
@@ -17,14 +18,14 @@ namespace FileTime.ConsoleUI.App
{
private readonly List<Tab> _tabs = new();
private readonly Dictionary<Tab, Render> _renderers = new();
private readonly Dictionary<Tab, TabState> _paneStates = new();
private readonly Dictionary<Tab, TabState> _tabStates = new();
private Tab? _selectedTab;
private readonly List<CommandBinding> _commandBindings = new();
private readonly IServiceProvider _serviceProvider;
private readonly IClipboard _clipboard;
private readonly IColoredConsoleRenderer _coloredConsoleRenderer;
private readonly CommandExecutor _commandExecutor;
private readonly TimeRunner _timeRunner;
private readonly ConsoleReader _consoleReader;
private readonly IStyles _styles;
private readonly List<ConsoleKeyInfo> _previousKeys = new();
@@ -35,14 +36,14 @@ namespace FileTime.ConsoleUI.App
IServiceProvider serviceProvider,
IClipboard clipboard,
IColoredConsoleRenderer coloredConsoleRenderer,
CommandExecutor commandExecutor,
TimeRunner timeRunner,
ConsoleReader consoleReader,
IStyles styles)
{
_serviceProvider = serviceProvider;
_clipboard = clipboard;
_coloredConsoleRenderer = coloredConsoleRenderer;
_commandExecutor = commandExecutor;
_timeRunner = timeRunner;
_consoleReader = consoleReader;
_styles = styles;
InitCommandBindings();
@@ -60,7 +61,7 @@ namespace FileTime.ConsoleUI.App
_tabs.Add(tab);
var paneState = new TabState(tab);
_paneStates.Add(tab, paneState);
_tabStates.Add(tab, paneState);
var renderer = _serviceProvider.GetService<Render>()!;
renderer.Init(tab, paneState);
@@ -73,7 +74,7 @@ namespace FileTime.ConsoleUI.App
{
_tabs.Remove(pane);
_renderers.Remove(pane);
_paneStates.Remove(pane);
_tabStates.Remove(pane);
}
private void InitCommandBindings()
@@ -134,7 +135,7 @@ namespace FileTime.ConsoleUI.App
Cut),
new CommandBinding(
"paste (merge)",
Commands.Paste,
Commands.PasteMerge,
new[]
{
new ConsoleKeyInfo('p', ConsoleKey.P, false, false, false),
@@ -143,7 +144,7 @@ namespace FileTime.ConsoleUI.App
PasteMerge),
new CommandBinding(
"paste (overwrite)",
Commands.Paste,
Commands.PasteOverwrite,
new[]
{
new ConsoleKeyInfo('p', ConsoleKey.P, false, false, false),
@@ -152,7 +153,7 @@ namespace FileTime.ConsoleUI.App
PasteOverwrite),
new CommandBinding(
"paste (skip)",
Commands.Paste,
Commands.PasteSkip,
new[]
{
new ConsoleKeyInfo('p', ConsoleKey.P, false, false, false),

View File

@@ -26,7 +26,7 @@ namespace FileTime.ConsoleUI.App.UI
public Tab Tab { get; private set; }
public TabState TabState { get; private set; }
public int PageSize => Console.WindowHeight - _contentPaddingTop - _contentPaddingBottom;
public static int PageSize => Console.WindowHeight - _contentPaddingTop - _contentPaddingBottom;
public Render(IColoredConsoleRenderer coloredRenderer, IStyles appStyle)
{
_coloredRenderer = coloredRenderer;
@@ -219,7 +219,7 @@ namespace FileTime.ConsoleUI.App.UI
}
}
var isSelected = TabState.ContainsSelectedItem(item.Provider, currentContainer, item.FullName!);
var isSelected = TabState.ContainsMarkedItem(currentContainer, new AbsolutePath(item));
if (isSelected)
{
backgroundColor = _appStyle.SelectedItemBackground;

View File

@@ -1,6 +1,6 @@
namespace AsyncEvent
{
public class AsyncEventHandler<TSender, TArg> where TArg : AsyncEventArgs
public class AsyncEventHandler<TSender, TArg>
{
private readonly List<Func<TSender, TArg, Task>> _handlers;
private readonly Action<Func<TSender, TArg, Task>> _add;
@@ -56,7 +56,7 @@
return obj;
}
}
public class AsyncEventHandler<TArg> : AsyncEventHandler<object?, AsyncEventArgs> where TArg : AsyncEventArgs
public class AsyncEventHandler<TArg> : AsyncEventHandler<object?, TArg>
{
public AsyncEventHandler(Action<Func<object?, TArg, Task>>? add = null, Action<Func<object?, TArg, Task>>? remove = null) : base(add, remove) { }
}

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;
_copyOperation?.Invoke(new AbsolutePath(element), AbsolutePath.FromParentAndChildName(target, targetName));
}
}
}
copy(new AbsolutePath(source.ContentProvider, element.FullName!), new AbsolutePath(target.Provider, targetPath));
}
}
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,6 +7,7 @@ namespace FileTime.Core.Components
{
private IItem? _currentSelectedItem;
private IContainer _currentLocation;
private string? _lastPath;
public int CurrentSelectedIndex { get; private set; }
@@ -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();
}
}

View File

@@ -15,12 +15,18 @@
<Color x:Key="ItemBackgroundColor">#00000000</Color>
<Color x:Key="AlternativeItemBackgroundColor">#10000000</Color>
<Color x:Key="SelectedItemBackgroundColor">#93a1a1</Color>
<Color x:Key="MarkedItemBackgroundColor">#00000000</Color>
<Color x:Key="MarkedAlternativeItemBackgroundColor">#10000000</Color>
<Color x:Key="MarkedSelectedItemBackgroundColor">#b58900</Color>
<Color x:Key="ForegroundColor">#93a1a1</Color>
<Color x:Key="AccentForegroundColor">#268bd2</Color>
<Color x:Key="LightForegroundColor">#7793a1a1</Color>
<Color x:Key="AlternativeForegroundColor">#93a1a1</Color>
<Color x:Key="AlternativeItemForegroundColor">#93a1a1</Color>
<Color x:Key="SelectedItemForegroundColor">#073642</Color>
<Color x:Key="MarkedItemForegroundColor">#b58900</Color>
<Color x:Key="MarkedAlternativeItemForegroundColor">#b58900</Color>
<Color x:Key="MarkedSelectedItemForegroundColor">#002b36</Color>
<Color x:Key="ErrorColor">#dc322f</Color>
@@ -48,6 +54,15 @@
<SolidColorBrush
x:Key="SelectedItemBackgroundBrush"
Color="{DynamicResource SelectedItemBackgroundColor}" />
<SolidColorBrush
x:Key="MarkedItemBackgroundBrush"
Color="{DynamicResource MarkedItemBackgroundColor}" />
<SolidColorBrush
x:Key="MarkedSelectedItemBackgroundBrush"
Color="{DynamicResource MarkedSelectedItemBackgroundColor}" />
<SolidColorBrush
x:Key="MarkedAlternativeItemBackgroundBrush"
Color="{DynamicResource MarkedAlternativeItemBackgroundColor}" />
<SolidColorBrush
x:Key="ForegroundBrush"
@@ -59,11 +74,20 @@
x:Key="LightForegroundBrush"
Color="{DynamicResource LightForegroundColor}" />
<SolidColorBrush
x:Key="AlternativeForegroundBrush"
Color="{DynamicResource AlternativeForegroundColor}" />
x:Key="AlternativeItemForegroundBrush"
Color="{DynamicResource AlternativeItemForegroundColor}" />
<SolidColorBrush
x:Key="SelectedItemForegroundBrush"
Color="{DynamicResource SelectedItemForegroundColor}" />
<SolidColorBrush
x:Key="MarkedItemForegroundBrush"
Color="{DynamicResource MarkedItemForegroundColor}" />
<SolidColorBrush
x:Key="MarkedAlternativeItemForegroundBrush"
Color="{DynamicResource MarkedAlternativeItemForegroundColor}" />
<SolidColorBrush
x:Key="MarkedSelectedItemForegroundBrush"
Color="{DynamicResource MarkedSelectedItemForegroundColor}" />
<SolidColorBrush
x:Key="ErrorBrush"
@@ -79,14 +103,23 @@
<converters:ItemViewModeToBrushConverter
x:Key="ItemViewModeToForegroundConverter"
DefaultBrush="{StaticResource ForegroundBrush}"
AlternativeBrush="{StaticResource AlternativeForegroundBrush}"
SelectedBrush="{StaticResource SelectedItemForegroundBrush}"/>
AlternativeBrush="{StaticResource AlternativeItemForegroundBrush}"
SelectedBrush="{StaticResource SelectedItemForegroundBrush}"
MarkedBrush="{StaticResource MarkedItemForegroundBrush}"
MarkedAlternativeBrush="{StaticResource MarkedAlternativeItemForegroundBrush}"
MarkedSelectedBrush="{StaticResource MarkedSelectedItemForegroundBrush}"/>
<converters:ItemViewModeToBrushConverter
x:Key="ItemViewModeToBackgroundConverter"
DefaultBrush="{StaticResource ItemBackgroundBrush}"
AlternativeBrush="{StaticResource AlternativeItemBackgroundBrush}"
SelectedBrush="{StaticResource SelectedItemBackgroundBrush}"/>
SelectedBrush="{StaticResource SelectedItemBackgroundBrush}"
MarkedBrush="{StaticResource MarkedItemBackgroundBrush}"
MarkedAlternativeBrush="{StaticResource MarkedAlternativeItemBackgroundBrush}"
MarkedSelectedBrush="{StaticResource MarkedSelectedItemBackgroundBrush}"/>
<converters:ContextMenuGenerator x:Key="ContextMenuGenerator"/>
<converters:ItemToImageConverter x:Key="ItemToImageConverter"/>
<converters:IsNullConverter x:Key="IsNullConverter"/>
<converters:IsNullConverter x:Key="IsNotNullConverter" Inverse="true"/>
</ResourceDictionary>
</Application.Resources>

View File

@@ -19,6 +19,7 @@ namespace FileTime.Avalonia
.RegisterDefaultServices()
.AddViewModels()
.AddServices()
.RegisterCommandHandlers()
.BuildServiceProvider();
}

View File

@@ -1,9 +1,11 @@
using FileTime.Core.Components;
using MvvmGen;
using System;
using MvvmGen;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Collections.Specialized;
using System.Linq;
using FileTime.App.Core.Tab;
using System.Threading.Tasks;
using FileTime.Core.Models;
namespace FileTime.Avalonia.Application
{
@@ -11,7 +13,7 @@ namespace FileTime.Avalonia.Application
public partial class AppState
{
[Property]
private ObservableCollection<TabContainer> _tabs = new ObservableCollection<TabContainer>();
private ObservableCollection<TabContainer> _tabs = new();
[Property]
private TabContainer _selectedTab;
@@ -24,7 +26,66 @@ namespace FileTime.Avalonia.Application
partial void OnInitialize()
{
_tabs.CollectionChanged += (o, e) => SelectedTab ??= Tabs.Count > 0 ? Tabs[0] : null;
_tabs.CollectionChanged += TabsChanged;
}
private void TabsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
SelectedTab ??= Tabs.Count > 0 ? Tabs[0] : null;
List<TabContainer> itemsAdded = new();
List<TabContainer> itemsRemoved = new();
if (e.NewItems != null && e.OldItems != null)
{
itemsAdded.AddRange(e.NewItems.Cast<TabContainer>().Except(e.OldItems.Cast<TabContainer>()));
itemsRemoved.AddRange(e.OldItems.Cast<TabContainer>().Except(e.NewItems.Cast<TabContainer>()));
}
else if (e.NewItems != null)
{
itemsAdded.AddRange(e.NewItems.Cast<TabContainer>());
}
else if (e.OldItems != null)
{
itemsRemoved.AddRange(e.OldItems.Cast<TabContainer>());
}
foreach (var item in itemsAdded)
{
item.TabState.ItemMarked.Add(TabItemMarked);
item.TabState.ItemUnmarked.Add(TabItemUnmarked);
}
foreach (var item in itemsRemoved)
{
item.TabState.ItemMarked.Remove(TabItemMarked);
item.TabState.ItemUnmarked.Remove(TabItemUnmarked);
}
}
private async Task TabItemMarked(TabState tabState, AbsolutePath item)
{
var tabContainer = Tabs.FirstOrDefault(t => t.TabState == tabState);
if (tabContainer != null)
{
var item2 = (await tabContainer.CurrentLocation.GetItems()).FirstOrDefault(i => i.Item.FullName == item.Path);
if (item2 != null)
{
item2.IsMarked = true;
}
}
}
private async Task TabItemUnmarked(TabState tabState, AbsolutePath item)
{
var tabContainer = Tabs.FirstOrDefault(t => t.TabState == tabState);
if (tabContainer != null)
{
var item2 = (await tabContainer.CurrentLocation.GetItems()).FirstOrDefault(i => i.Item.FullName == item.Path);
if (item2 != null)
{
item2.IsMarked = false;
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using FileTime.Avalonia.ViewModels;
namespace FileTime.Avalonia.Application
{
public interface INewItemProcessor
{
Task UpdateMarkedItems(ContainerViewModel containerViewModel);
}
}

View File

@@ -6,11 +6,11 @@ using FileTime.Avalonia.Services;
using FileTime.Avalonia.ViewModels;
using MvvmGen;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FileTime.App.Core.Tab;
using System.Collections.Generic;
namespace FileTime.Avalonia.Application
{
@@ -18,8 +18,11 @@ namespace FileTime.Avalonia.Application
[Inject(typeof(ItemNameConverterService))]
[Inject(typeof(LocalContentProvider))]
[Inject(typeof(Tab))]
public partial class TabContainer
public partial class TabContainer : INewItemProcessor
{
[Property]
private TabState _tabState;
[Property]
private ContainerViewModel _parent;
@@ -45,25 +48,33 @@ namespace FileTime.Avalonia.Application
if (_selectedItem != value)// && value != null
{
_selectedItem = value;
OnPropertyChanged("SelectedItem");
SelectedItemChanged();
}
}
}
partial void OnInitialize()
{
_tabState = new TabState(Tab);
}
public async Task Init(int tabNumber)
{
TabNumber = tabNumber;
Tab.CurrentLocationChanged.Add(Tab_CurrentLocationChanged);
Tab.CurrentSelectedItemChanged.Add(Tab_CurrentSelectedItemChanged);
CurrentLocation = new ContainerViewModel(await Tab.GetCurrentLocation(), ItemNameConverterService);
var currentLocation = await Tab.GetCurrentLocation();
var parent = GenerateParent(currentLocation);
CurrentLocation = new ContainerViewModel(this, parent, currentLocation, ItemNameConverterService);
await CurrentLocation.Init();
var parent = (await Tab.GetCurrentLocation()).GetParent();
if (parent != null)
{
Parent = new ContainerViewModel(parent, ItemNameConverterService);
parent.ChildrenToAdopt.Add(CurrentLocation);
Parent = parent;
await Parent.Init();
}
else
@@ -74,16 +85,28 @@ namespace FileTime.Avalonia.Application
await UpdateCurrentSelectedItem();
}
private ContainerViewModel? GenerateParent(IContainer? container, bool recursive = false)
{
var parentContainer = container?.GetParent();
if (parentContainer == null) return null;
var parentParent = recursive ? GenerateParent(parentContainer.GetParent(), recursive) : null;
var parent = new ContainerViewModel(this, parentParent, parentContainer, ItemNameConverterService);
parentParent?.ChildrenToAdopt.Add(parent);
return parent;
}
private async Task Tab_CurrentLocationChanged(object? sender, AsyncEventArgs e)
{
var currentLocation = await Tab.GetCurrentLocation();
CurrentLocation = new ContainerViewModel(currentLocation, ItemNameConverterService);
var parent = GenerateParent(currentLocation);
CurrentLocation = new ContainerViewModel(this, parent, currentLocation, ItemNameConverterService);
await CurrentLocation.Init();
var parent = currentLocation.GetParent();
if (parent != null)
{
Parent = new ContainerViewModel(parent, ItemNameConverterService);
parent.ChildrenToAdopt.Add(CurrentLocation);
Parent = parent;
await Parent.Init();
}
else
@@ -97,7 +120,9 @@ namespace FileTime.Avalonia.Application
await UpdateCurrentSelectedItem();
}
private async Task UpdateCurrentSelectedItem()
public async Task UpdateCurrentSelectedItem()
{
try
{
var tabCurrentSelectenItem = await Tab.GetCurrentSelectedItem();
IItemViewModel? currentSelectenItem = null;
@@ -127,9 +152,57 @@ namespace FileTime.Avalonia.Application
}
var items = await _currentLocation.GetItems();
if (items != null && items.Count > 0)
{
foreach (var item in items)
{
item.IsSelected = item == currentSelectenItem;
var isSelected = item == currentSelectenItem;
item.IsSelected = isSelected;
if (isSelected)
{
var parent = item.Parent;
while (parent != null)
{
parent.IsSelected = true;
parent = parent.Parent;
}
try
{
var child = item;
while (child is ContainerViewModel containerViewModel && containerViewModel.Container.IsLoaded)
{
var activeChildItem = await Tab.GetItemByLastPath(containerViewModel.Container);
child = (await containerViewModel.GetItems()).FirstOrDefault(i => i.Item == activeChildItem);
if (child != null)
{
child.IsSelected = true;
}
}
}
catch
{
//INFO collection modified exception on: child = (await containerViewModel.GetItems()).FirstOrDefault(i => i.Item == activeChildItem);
//TODO: handle or error message
}
}
}
}
else
{
var parent = _currentLocation;
while (parent != null)
{
parent.IsSelected = true;
parent = parent.Parent;
}
}
}
catch
{
//INFO collection modified exception on: currentSelectenItem = (await _currentLocation.GetItems()).FirstOrDefault(i => i.Item.Name == tabCurrentSelectenItem.Name);
//TODO: handle or error message
}
}
@@ -212,9 +285,32 @@ namespace FileTime.Avalonia.Application
(await Tab.GetCurrentLocation())?.CreateContainer(name);
}
public async Task CreateElement(string name)
{
(await Tab.GetCurrentLocation())?.CreateElement(name);
}
public async Task OpenContainer(IContainer container)
{
await Tab.OpenContainer(container);
}
public async Task MarkCurrentItem()
{
await _tabState.MakrCurrentItem();
}
public async Task UpdateMarkedItems(ContainerViewModel containerViewModel)
{
if (containerViewModel == CurrentLocation && containerViewModel.Container.IsLoaded)
{
var selectedItems = TabState.GetCurrentMarkedItems(containerViewModel.Container);
foreach (var item in await containerViewModel.GetItems())
{
item.IsMarked = selectedItems.Any(c => c.Path == item.Item.FullName);
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
using Avalonia.Data.Converters;
using System;
using System.Globalization;
namespace FileTime.Avalonia.Converters
{
public class IsNullConverter : IValueConverter
{
public bool Inverse { get; set; }
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
var result = value == null;
if (Inverse) result = !result;
return result;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Svg.Skia;
using FileTime.Avalonia.IconProviders;
using FileTime.Avalonia.ViewModels;
using FileTime.Core.Models;
namespace FileTime.Avalonia.Converters
{
public class ItemToImageConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value == null) return null;
IIconProvider converter = new MaterialIconProvider();
IItem item = value switch
{
ContainerViewModel container => container.Container,
ElementViewModel element => element.Element,
_ => throw new NotImplementedException()
};
var path = converter.GetImage(item)!;
var source = SvgSource.Load<SvgSource>("avares://FileTime.Avalonia" + path, null);
return new SvgImage { Source = source };
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -11,6 +11,9 @@ namespace FileTime.Avalonia.Converters
public Brush? DefaultBrush { get; set; }
public Brush? AlternativeBrush { get; set; }
public Brush? SelectedBrush { get; set; }
public Brush? MarkedBrush { get; set; }
public Brush? MarkedSelectedBrush { get; set; }
public Brush? MarkedAlternativeBrush { get; set; }
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
@@ -21,6 +24,9 @@ namespace FileTime.Avalonia.Converters
ItemViewMode.Default => DefaultBrush,
ItemViewMode.Alternative => AlternativeBrush,
ItemViewMode.Selected => SelectedBrush,
ItemViewMode.Marked => MarkedBrush,
ItemViewMode.MarkedSelected => MarkedSelectedBrush,
ItemViewMode.MarkedAlternative => MarkedAlternativeBrush,
_ => throw new NotImplementedException()
};
}

View File

@@ -0,0 +1,9 @@
using FileTime.Core.Models;
namespace FileTime.Avalonia.IconProviders
{
public interface IIconProvider
{
string GetImage(IItem item);
}
}

View File

@@ -0,0 +1,33 @@
using FileTime.Core.Models;
using FileTime.Providers.Local;
using System.Linq;
namespace FileTime.Avalonia.IconProviders
{
public class MaterialIconProvider : IIconProvider
{
public string GetImage(IItem item)
{
var icon = "file.svg";
if (item is IContainer)
{
icon = "folder.svg";
}
else if (item is IElement element)
{
if(element is LocalFile localFile && element.FullName.EndsWith(".svg"))
{
return localFile.File.FullName;
}
icon = !element.Name.Contains('.')
? icon
: element.Name.Split('.').Last() switch
{
"cs" => "csharp.svg",
_ => icon
};
}
return "/Assets/material/" + icon;
}
}
}

View File

@@ -12,9 +12,10 @@ namespace FileTime.Avalonia.Misc
public string Value { get; set; }
public InputElementWrapper(InputElement inputElement)
public InputElementWrapper(InputElement inputElement, string? defaultValue = null)
{
InputElement = inputElement;
Value = defaultValue ?? "";
}
}
}

View File

@@ -3,6 +3,7 @@ using FileTime.Avalonia.Application;
using FileTime.Avalonia.Models;
using FileTime.Avalonia.ViewModels;
using MvvmGen;
using System;
using System.Collections.Generic;
namespace FileTime.Avalonia.Services
@@ -20,9 +21,8 @@ namespace FileTime.Avalonia.Services
{
var nameLeft = itemViewModel.Item.Name;
while (nameLeft.ToLower().Contains(rapidTravelText))
while (nameLeft.ToLower().IndexOf(rapidTravelText, StringComparison.Ordinal) is int rapidTextStart && rapidTextStart != -1)
{
var rapidTextStart = nameLeft.ToLower().IndexOf(rapidTravelText);
var before = rapidTextStart > 0 ? nameLeft.Substring(0, rapidTextStart) : null;
var rapidTravel = nameLeft.Substring(rapidTextStart, rapidTravelText.Length);

View File

@@ -2,6 +2,7 @@
using FileTime.Avalonia.Application;
using FileTime.Avalonia.Services;
using FileTime.Avalonia.ViewModels;
using FileTime.Core.Command;
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.Avalonia
@@ -29,6 +30,15 @@ namespace FileTime.Avalonia
throw new System.Exception("TODO: implement linux contextmenu provider");
}
return serviceCollection;
}
internal static IServiceCollection RegisterCommandHandlers(this IServiceCollection serviceCollection)
{
foreach (var commandHandler in FileTime.Providers.Local.Startup.GetCommandHandlers())
{
serviceCollection.AddTransient(typeof(ICommandHandler), commandHandler);
}
return serviceCollection;
}
}

View File

@@ -9,6 +9,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FileTime.Avalonia.Application;
namespace FileTime.Avalonia.ViewModels
{
@@ -16,8 +17,9 @@ namespace FileTime.Avalonia.ViewModels
[Inject(typeof(ItemNameConverterService))]
public partial class ContainerViewModel : IItemViewModel
{
private bool isRefreshing;
private bool isInitialized;
private bool _isRefreshing;
private bool _isInitialized;
private INewItemProcessor _newItemProcessor;
[Property]
private IContainer _container;
@@ -25,29 +27,39 @@ namespace FileTime.Avalonia.ViewModels
[Property]
private bool _isSelected;
public IItem Item => _container;
//[Property]
private readonly ObservableCollection<ContainerViewModel> _containers = new ObservableCollection<ContainerViewModel>();
//[Property]
private readonly ObservableCollection<ElementViewModel> _elements = new ObservableCollection<ElementViewModel>();
//[Property]
private readonly ObservableCollection<IItemViewModel> _items = new ObservableCollection<IItemViewModel>();
[Property]
private bool _isAlternative;
[Property]
private bool _isMarked;
[Property]
private ContainerViewModel? _parent;
public IItem Item => _container;
private readonly ObservableCollection<ContainerViewModel> _containers = new ObservableCollection<ContainerViewModel>();
private readonly ObservableCollection<ElementViewModel> _elements = new ObservableCollection<ElementViewModel>();
private readonly ObservableCollection<IItemViewModel> _items = new ObservableCollection<IItemViewModel>();
public List<IItemViewModel> ChildrenToAdopt { get; } = new List<IItemViewModel>();
[PropertyInvalidate(nameof(IsSelected))]
[PropertyInvalidate(nameof(IsAlternative))]
[PropertyInvalidate(nameof(IsMarked))]
public ItemViewMode ViewMode =>
IsSelected
? ItemViewMode.Selected
: IsAlternative
? ItemViewMode.Alternative
: ItemViewMode.Default;
(IsMarked, IsSelected, IsAlternative) switch
{
(true, true, _) => ItemViewMode.MarkedSelected,
(true, false, true) => ItemViewMode.MarkedAlternative,
(false, true, _) => ItemViewMode.Selected,
(false, false, true) => ItemViewMode.Alternative,
(true, false, false) => ItemViewMode.Marked,
_ => ItemViewMode.Default
};
public List<ItemNamePart> DisplayName => ItemNameConverterService.GetDisplayName(this);
@@ -56,7 +68,7 @@ namespace FileTime.Avalonia.ViewModels
{
get
{
if (!isInitialized) Task.Run(Refresh);
if (!_isInitialized) Task.Run(Refresh);
return _containers;
}
}
@@ -66,7 +78,7 @@ namespace FileTime.Avalonia.ViewModels
{
get
{
if (!isInitialized) Task.Run(Refresh);
if (!_isInitialized) Task.Run(Refresh);
return _elements;
}
}
@@ -76,13 +88,16 @@ namespace FileTime.Avalonia.ViewModels
{
get
{
if (!isInitialized) Task.Run(Refresh);
if (!_isInitialized) Task.Run(Refresh);
return _items;
}
}
public ContainerViewModel(IContainer container, ItemNameConverterService itemNameConverterService) : this(itemNameConverterService)
public ContainerViewModel(INewItemProcessor newItemProcessor, ContainerViewModel? parent, IContainer container, ItemNameConverterService itemNameConverterService) : this(itemNameConverterService)
{
_newItemProcessor = newItemProcessor;
Parent = parent;
Container = container;
Container.Refreshed.Add(Container_Refreshed);
}
@@ -94,7 +109,7 @@ namespace FileTime.Avalonia.ViewModels
await Refresh(initializeChildren);
}
private async Task Container_Refreshed(object sender, AsyncEventArgs e)
private async Task Container_Refreshed(object? sender, AsyncEventArgs e)
{
await Refresh(false);
}
@@ -105,16 +120,16 @@ namespace FileTime.Avalonia.ViewModels
}
private async Task Refresh(bool initializeChildren)
{
if (isRefreshing) return;
if (_isRefreshing) return;
isInitialized = true;
_isInitialized = true;
try
{
isRefreshing = true;
_isRefreshing = true;
var containers = (await _container.GetContainers()).Select(c => new ContainerViewModel(c, ItemNameConverterService)).ToList();
var elements = (await _container.GetElements()).Select(e => new ElementViewModel(e, ItemNameConverterService)).ToList();
var containers = (await _container.GetContainers()).Select(c => AdoptOrCreateItem(c, (c2) => new ContainerViewModel(_newItemProcessor, this, c2, ItemNameConverterService))).ToList();
var elements = (await _container.GetElements()).Select(e => AdoptOrCreateItem(e, (e2) => new ElementViewModel(e2, this, ItemNameConverterService))).ToList();
_containers.Clear();
_elements.Clear();
@@ -141,24 +156,51 @@ namespace FileTime.Avalonia.ViewModels
}
catch { }
isRefreshing = false;
await _newItemProcessor.UpdateMarkedItems(this);
_isRefreshing = false;
}
private TResult AdoptOrCreateItem<T, TResult>(T item, Func<T, TResult> generator) where T : IItem
{
var itemToAdopt = ChildrenToAdopt.Find(i => i.Item.Name == item.Name);
if (itemToAdopt is TResult itemViewModel) return itemViewModel;
return generator(item);
}
public void Unload(bool recursive = true)
{
_isInitialized = false;
if (recursive)
{
foreach (var container in _containers)
{
container.Unload(true);
container.ChildrenToAdopt.Clear();
}
}
_containers.Clear();
_elements.Clear();
_items.Clear();
}
public async Task<ObservableCollection<ContainerViewModel>> GetContainers()
{
if (!isInitialized) await Task.Run(Refresh);
if (!_isInitialized) await Task.Run(Refresh);
return _containers;
}
public async Task<ObservableCollection<ElementViewModel>> GetElements()
{
if (!isInitialized) await Task.Run(Refresh);
if (!_isInitialized) await Task.Run(Refresh);
return _elements;
}
public async Task<ObservableCollection<IItemViewModel>> GetItems()
{
if (!isInitialized) await Task.Run(Refresh);
if (!_isInitialized) await Task.Run(Refresh);
return _items;
}
}

View File

@@ -21,20 +21,32 @@ namespace FileTime.Avalonia.ViewModels
[Property]
private bool _isAlternative;
[Property]
private bool _isMarked;
[Property]
private ContainerViewModel? _parent;
[PropertyInvalidate(nameof(IsSelected))]
[PropertyInvalidate(nameof(IsAlternative))]
[PropertyInvalidate(nameof(IsMarked))]
public ItemViewMode ViewMode =>
IsSelected
? ItemViewMode.Selected
: IsAlternative
? ItemViewMode.Alternative
: ItemViewMode.Default;
(IsMarked, IsSelected, IsAlternative) switch
{
(true, true, _) => ItemViewMode.MarkedSelected,
(true, false, true) => ItemViewMode.MarkedAlternative,
(false, true, _) => ItemViewMode.Selected,
(false, false, true) => ItemViewMode.Alternative,
(true, false, false) => ItemViewMode.Marked,
_ => ItemViewMode.Default
};
public List<ItemNamePart> DisplayName => ItemNameConverterService.GetDisplayName(this);
public ElementViewModel(IElement element, ItemNameConverterService itemNameConverterService) : this(itemNameConverterService)
public ElementViewModel(IElement element, ContainerViewModel parent, ItemNameConverterService itemNameConverterService) : this(itemNameConverterService)
{
Element = element;
Parent = parent;
}
public void InvalidateDisplayName() => OnPropertyChanged(nameof(DisplayName));

View File

@@ -1,8 +1,6 @@
using FileTime.Core.Models;
using FileTime.Avalonia.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace FileTime.Avalonia.ViewModels
{
@@ -12,6 +10,8 @@ namespace FileTime.Avalonia.ViewModels
bool IsSelected { get; set; }
bool IsAlternative { get; set; }
bool IsMarked { get; set; }
ContainerViewModel? Parent{ get; set; }
ItemViewMode ViewMode { get; }

View File

@@ -6,8 +6,11 @@ namespace FileTime.Avalonia.ViewModels
{
public enum ItemViewMode
{
Selected,
Default,
Alternative,
Default
Selected,
Marked,
MarkedSelected,
MarkedAlternative
}
}

View File

@@ -18,6 +18,10 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Input;
using FileTime.App.Core.Clipboard;
using Microsoft.Extensions.DependencyInjection;
using FileTime.Core.Command;
using FileTime.Core.Timeline;
namespace FileTime.Avalonia.ViewModels
{
@@ -34,6 +38,9 @@ namespace FileTime.Avalonia.ViewModels
private List<CommandBinding> _commandBindings = new();
private List<CommandBinding> _universalCommandBindings = new();
private IClipboard _clipboard;
private TimeRunner _timeRunner;
private Action? _inputHandler;
[Property]
@@ -51,10 +58,16 @@ namespace FileTime.Avalonia.ViewModels
[Property]
private List<RootDriveInfo> _rootDriveInfos;
public Action? FocusDefaultElement { get; set; }
[Property]
private string _messageBoxText;
public IReadOnlyList<ReadOnlyParallelCommands> TimelineCommands => _timeRunner.ParallelCommands;
async partial void OnInitialize()
{
_clipboard = App.ServiceProvider.GetService<IClipboard>()!;
_timeRunner = App.ServiceProvider.GetService<TimeRunner>()!;
_timeRunner.CommandsChanged += (o, e) => OnPropertyChanged(nameof(TimelineCommands));
InitCommandBindings();
_keysToSkip.Add(new KeyWithModifiers[] { new KeyWithModifiers(Key.Up) });
@@ -62,6 +75,7 @@ namespace FileTime.Avalonia.ViewModels
_keysToSkip.Add(new KeyWithModifiers[] { new KeyWithModifiers(Key.Tab) });
_keysToSkip.Add(new KeyWithModifiers[] { new KeyWithModifiers(Key.PageDown) });
_keysToSkip.Add(new KeyWithModifiers[] { new KeyWithModifiers(Key.PageUp) });
_keysToSkip.Add(new KeyWithModifiers[] { new KeyWithModifiers(Key.F4, alt: true) });
var tab = new Tab();
await tab.Init(LocalContentProvider);
@@ -176,7 +190,6 @@ namespace FileTime.Avalonia.ViewModels
_previousKeys.Clear();
PossibleCommands = new();
FocusDefaultElement?.Invoke();
return Task.CompletedTask;
}
@@ -190,7 +203,6 @@ namespace FileTime.Avalonia.ViewModels
AppState.RapidTravelText = "";
await AppState.SelectedTab.OpenContainer(await AppState.SelectedTab.CurrentLocation.Container.WithoutVirtualContainer(RAPIDTRAVEL));
FocusDefaultElement?.Invoke();
}
public async Task SwitchToTab(int number)
@@ -260,7 +272,9 @@ namespace FileTime.Avalonia.ViewModels
{
if (Inputs != null)
{
AppState.SelectedTab.CreateContainer(Inputs[0].Value).Wait();
var container = AppState.SelectedTab.CurrentLocation.Container;
var createContainerCommand = new CreateContainerCommand(new Core.Models.AbsolutePath(container), Inputs[0].Value);
_timeRunner.AddCommand(createContainerCommand).Wait();
Inputs = null;
}
};
@@ -270,10 +284,222 @@ namespace FileTime.Avalonia.ViewModels
return Task.CompletedTask;
}
public Task CreateElement()
{
var handler = () =>
{
if (Inputs != null)
{
var container = AppState.SelectedTab.CurrentLocation.Container;
var createElementCommand = new CreateElementCommand(new Core.Models.AbsolutePath(container), Inputs[0].Value);
_timeRunner.AddCommand(createElementCommand).Wait();
Inputs = null;
}
};
ReadInputs(new List<Core.Interactions.InputElement>() { new Core.Interactions.InputElement("Element name", InputType.Text) }, handler);
return Task.CompletedTask;
}
public async Task MarkCurrentItem()
{
await AppState.SelectedTab.MarkCurrentItem();
}
public async Task Copy()
{
_clipboard.Clear();
_clipboard.SetCommand<CopyCommand>();
var currentSelectedItems = await AppState.SelectedTab.TabState.GetCurrentMarkedItems();
if (currentSelectedItems.Count > 0)
{
foreach (var selectedItem in currentSelectedItems)
{
_clipboard.AddContent(selectedItem);
}
}
else
{
var currentSelectedItem = AppState.SelectedTab.SelectedItem?.Item;
if (currentSelectedItem != null)
{
_clipboard.AddContent(new AbsolutePath(currentSelectedItem));
}
}
}
public Task Cut()
{
_clipboard.Clear();
_clipboard.SetCommand<MoveCommand>();
return Task.CompletedTask;
}
public async Task Delete()
{
IList<Core.Models.AbsolutePath>? itemsToDelete = null;
var askForDelete = false;
var questionText = "";
var shouldDelete = false;
var currentSelectedItems = await AppState.SelectedTab.TabState.GetCurrentMarkedItems();
var currentSelectedItem = AppState.SelectedTab.SelectedItem?.Item;
if (currentSelectedItems.Count > 0)
{
itemsToDelete = currentSelectedItems.Cast<Core.Models.AbsolutePath>().ToList();
//FIXME: check 'is Container'
if (currentSelectedItems.Count == 1)
{
if ((await currentSelectedItems[0].Resolve()) is IContainer container
&& (await container.GetItems())?.Count > 0)
{
askForDelete = true;
questionText = $"The container '{container.Name}' is not empty. Proceed with delete?";
}
else
{
shouldDelete = true;
}
}
else
{
askForDelete = true;
questionText = $"Are you sure you want to delete {itemsToDelete.Count} item?";
}
}
else if (currentSelectedItem != null)
{
itemsToDelete = new List<Core.Models.AbsolutePath>()
{
new Core.Models.AbsolutePath(currentSelectedItem)
};
if (currentSelectedItem is IContainer container && (await container.GetItems())?.Count > 0)
{
askForDelete = true;
questionText = $"The container '{container.Name}' is not empty. Proceed with delete?";
}
else
{
shouldDelete = true;
}
}
if (itemsToDelete?.Count > 0)
{
if (askForDelete)
{
ShowMessageBox(questionText, HandleDelete);
}
else if (shouldDelete)
{
HandleDelete();
}
}
void HandleDelete()
{
var deleteCommand = new DeleteCommand();
foreach (var itemToDelete in itemsToDelete!)
{
deleteCommand.ItemsToDelete.Add(itemToDelete);
}
_timeRunner.AddCommand(deleteCommand).Wait();
_clipboard.Clear();
}
}
public async Task PasteMerge()
{
await Paste(TransportMode.Merge);
}
public async Task PasteOverwrite()
{
await Paste(TransportMode.Overwrite);
}
public async Task PasteSkip()
{
await Paste(TransportMode.Skip);
}
private async Task Paste(TransportMode transportMode)
{
if (_clipboard.CommandType != null)
{
var command = (ITransportationCommand)Activator.CreateInstance(_clipboard.CommandType!)!;
command.TransportMode = transportMode;
command.Sources.Clear();
foreach (var item in _clipboard.Content)
{
command.Sources.Add(item);
}
var currentLocation = AppState.SelectedTab.CurrentLocation.Container;
command.Target = currentLocation is VirtualContainer virtualContainer
? virtualContainer.BaseContainer
: currentLocation;
await _timeRunner.AddCommand(command);
_clipboard.Clear();
}
}
private Task Rename()
{
var selectedItem = AppState.SelectedTab.SelectedItem?.Item;
if (selectedItem != null)
{
var handler = () =>
{
if (Inputs != null)
{
var renameCommand = new RenameCommand(new Core.Models.AbsolutePath(selectedItem), Inputs[0].Value);
_timeRunner.AddCommand(renameCommand).Wait();
}
};
ReadInputs(new List<Core.Interactions.InputElement>() { new Core.Interactions.InputElement("New name", InputType.Text, selectedItem.Name) }, handler);
}
return Task.CompletedTask;
}
private async Task RefreshCurrentLocation()
{
await AppState.SelectedTab.CurrentLocation.Container.Refresh();
await AppState.SelectedTab.UpdateCurrentSelectedItem();
}
private Task PauseTimeline()
{
_timeRunner.EnableRunning = false;
return Task.CompletedTask;
}
private async Task ContinueTimeline()
{
_timeRunner.EnableRunning = true;
await _timeRunner.TryStartCommandRunner();
}
private async Task RefreshTimeline()
{
await _timeRunner.Refresh();
}
[Command]
public void ProcessInputs()
{
_inputHandler();
_inputHandler?.Invoke();
Inputs = null;
_inputHandler = null;
@@ -286,8 +512,31 @@ namespace FileTime.Avalonia.ViewModels
_inputHandler = null;
}
[Command]
public void ProcessMessageBoxCommand()
{
_inputHandler?.Invoke();
MessageBoxText = null;
_inputHandler = null;
}
[Command]
public void CancelMessageBoxCommand()
{
MessageBoxText = null;
_inputHandler = null;
}
public async Task<bool> ProcessKeyDown(Key key, KeyModifiers keyModifiers)
{
if (key == Key.LeftAlt
|| key == Key.RightAlt
|| key == Key.LeftShift
|| key == Key.RightShift
|| key == Key.LeftCtrl
|| key == Key.RightCtrl) return false;
NoCommandFound = false;
var isAltPressed = (keyModifiers & KeyModifiers.Alt) == KeyModifiers.Alt;
@@ -312,13 +561,12 @@ namespace FileTime.Avalonia.ViewModels
await selectedCommandBinding.InvokeAsync();
_previousKeys.Clear();
PossibleCommands = new();
FocusDefaultElement?.Invoke();
}
else if (_keysToSkip.Any(k => AreKeysEqual(k, _previousKeys)))
{
_previousKeys.Clear();
PossibleCommands = new();
return false;
}
else if (_previousKeys.Count == 2)
{
@@ -370,7 +618,11 @@ namespace FileTime.Avalonia.ViewModels
if (selectedCommandBinding != null)
{
await selectedCommandBinding.InvokeAsync();
FocusDefaultElement?.Invoke();
return true;
}
else
{
return false;
}
}
@@ -399,8 +651,6 @@ namespace FileTime.Avalonia.ViewModels
{
await AppState.SelectedTab.MoveCursorToFirst();
}
FocusDefaultElement?.Invoke();
}
}
@@ -415,7 +665,13 @@ namespace FileTime.Avalonia.ViewModels
private void ReadInputs(List<Core.Interactions.InputElement> inputs, Action inputHandler)
{
Inputs = inputs.Select(i => new InputElementWrapper(i)).ToList();
Inputs = inputs.Select(i => new InputElementWrapper(i, i.DefaultValue)).ToList();
_inputHandler = inputHandler;
}
private void ShowMessageBox(string text, Action inputHandler)
{
MessageBoxText = text;
_inputHandler = inputHandler;
}
@@ -451,6 +707,11 @@ namespace FileTime.Avalonia.ViewModels
FileTime.App.Core.Command.Commands.CreateContainer,
new KeyWithModifiers[]{new KeyWithModifiers(Key.C),new KeyWithModifiers(Key.C)},
CreateContainer),
new CommandBinding(
"create element",
FileTime.App.Core.Command.Commands.CreateElement,
new KeyWithModifiers[]{new KeyWithModifiers(Key.C),new KeyWithModifiers(Key.E)},
CreateElement),
new CommandBinding(
"move to first",
FileTime.App.Core.Command.Commands.MoveToTop,
@@ -526,6 +787,66 @@ namespace FileTime.Avalonia.ViewModels
FileTime.App.Core.Command.Commands.GoToHome,
new KeyWithModifiers[]{new KeyWithModifiers(Key.Q)},
CloseTab),
new CommandBinding(
"select",
FileTime.App.Core.Command.Commands.Select,
new KeyWithModifiers[]{new KeyWithModifiers(Key.Space)},
MarkCurrentItem),
new CommandBinding(
"copy",
FileTime.App.Core.Command.Commands.Copy,
new KeyWithModifiers[]{new KeyWithModifiers(Key.Y),new KeyWithModifiers(Key.Y)},
Copy),
new CommandBinding(
"cut",
FileTime.App.Core.Command.Commands.Cut,
new KeyWithModifiers[]{new KeyWithModifiers(Key.D),new KeyWithModifiers(Key.D)},
Cut),
new CommandBinding(
"delete",
FileTime.App.Core.Command.Commands.Delete,
new KeyWithModifiers[]{new KeyWithModifiers(Key.D),new KeyWithModifiers(Key.D, shift: true)},
Delete),
new CommandBinding(
"paste merge",
FileTime.App.Core.Command.Commands.PasteMerge,
new KeyWithModifiers[]{new KeyWithModifiers(Key.P),new KeyWithModifiers(Key.P)},
PasteMerge),
new CommandBinding(
"paste (overwrite)",
FileTime.App.Core.Command.Commands.PasteOverwrite,
new KeyWithModifiers[]{new KeyWithModifiers(Key.P),new KeyWithModifiers(Key.O)},
PasteOverwrite),
new CommandBinding(
"paste (skip)",
FileTime.App.Core.Command.Commands.PasteSkip,
new KeyWithModifiers[]{new KeyWithModifiers(Key.P),new KeyWithModifiers(Key.S)},
PasteSkip),
new CommandBinding(
"rename",
FileTime.App.Core.Command.Commands.Rename,
new KeyWithModifiers[]{new KeyWithModifiers(Key.C),new KeyWithModifiers(Key.W)},
Rename),
new CommandBinding(
"timeline pause",
FileTime.App.Core.Command.Commands.Dummy,
new KeyWithModifiers[]{new KeyWithModifiers(Key.T),new KeyWithModifiers(Key.P)},
PauseTimeline),
new CommandBinding(
"timeline start",
FileTime.App.Core.Command.Commands.Dummy,
new KeyWithModifiers[]{new KeyWithModifiers(Key.T),new KeyWithModifiers(Key.S)},
ContinueTimeline),
new CommandBinding(
"refresh timeline",
FileTime.App.Core.Command.Commands.Dummy,
new KeyWithModifiers[]{new KeyWithModifiers(Key.T),new KeyWithModifiers(Key.R)},
RefreshTimeline),
new CommandBinding(
"refresh",
FileTime.App.Core.Command.Commands.Refresh,
new KeyWithModifiers[]{new KeyWithModifiers(Key.R)},
RefreshCurrentLocation),
};
var universalCommandBindings = new List<CommandBinding>()
{

View File

@@ -17,7 +17,7 @@
Height="18"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Source="{SvgImage /Assets/material/folder.svg}" />
Source="{Binding Converter={StaticResource ItemToImageConverter}}" />
<ItemsControl
Grid.Column="1"

View File

@@ -89,8 +89,29 @@
</Border>
</Grid>
<Grid Grid.Column="1" RowDefinitions="40,*,Auto">
<Grid ColumnDefinitions="*,Auto">
<Grid Grid.Column="2" RowDefinitions="Auto,40,*,Auto">
<Grid>
<ItemsControl Items="{Binding TimelineCommands}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl Items="{Binding Commands}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto">
<StackPanel Orientation="Horizontal">
<TextBlock
@@ -132,7 +153,7 @@
</Grid>
<Grid
Grid.Row="1"
Grid.Row="2"
Margin="20,0,0,0">
<Grid>
<Grid.ColumnDefinitions>
@@ -146,8 +167,8 @@
<Grid>
<ListBox
Classes="ContentListView"
Items="{Binding AppState.SelectedTab.Parent.Items}"
SelectedItem="{Binding AppState.SelectedTab.CurrentLocation}">
IsEnabled="False"
Items="{Binding AppState.SelectedTab.Parent.Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<local:ItemView ShowAttributes="False"/>
@@ -202,6 +223,7 @@
<Grid Grid.Column="4">
<ListBox
Classes="ContentListView"
IsEnabled="False"
x:Name="ChildItems"
Items="{Binding AppState.SelectedTab.ChildContainer.Items}"
IsVisible="{Binding AppState.SelectedTab.ChildContainer.Items.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}">
@@ -223,9 +245,78 @@
</TextBlock>
</Grid>
</Grid>
<Grid
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsVisible="{Binding Inputs, Converter={StaticResource IsNotNullConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ItemsControl
x:Name="InputList"
Items="{Binding Inputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid MinWidth="400">
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding InputElement.Text}" />
<TextBox
AttachedToVisualTree="InputText_AttachedToVisualTree"
Grid.Column="1"
GotFocus="InputText_GotFocus"
LostFocus="InputText_LostFocus"
KeyDown="InputText_KeyDown"
Text="{Binding Value, Mode=TwoWay}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel
Grid.Row="1"
Orientation="Horizontal">
<Button
Command="{Binding ProcessInputsCommand}"
Content="Ok" />
<Button
Command="{Binding CancelInputsCommand}"
Content="Cancel" />
</StackPanel>
</Grid>
<Grid Grid.Row="2">
<Grid
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsVisible="{Binding MessageBoxText, Converter={StaticResource IsNotNullConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="{Binding MessageBoxText}"/>
<StackPanel
Grid.Row="1"
Orientation="Horizontal">
<Button
Command="{Binding ProcessMessageBoxCommand}"
Content="Yes" />
<Button
Command="{Binding CancelMessageBoxCommand}"
Content="No" />
</StackPanel>
</Grid>
</Grid>
<Grid Grid.Row="3">
<Grid IsVisible="{Binding AppState.ViewMode, Converter={StaticResource EqualityConverter}, ConverterParameter=RapidTravel}">
<Grid.RowDefinitions>
<RowDefinition Height="1" />

View File

@@ -1,8 +1,11 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using FileTime.Avalonia.Misc;
using FileTime.Avalonia.ViewModels;
using System.Linq;
namespace FileTime.Avalonia.Views
{
@@ -15,20 +18,12 @@ namespace FileTime.Avalonia.Views
{
if (value != DataContext)
{
if (DataContext is MainPageViewModel currentViewModel)
{
currentViewModel.FocusDefaultElement = null;
}
DataContext = value;
}
}
}
if (value != null)
{
//value.FocusDefaultElement = () => this.FindControl<ListBox>("CurrentItems")?.Focus();
}
}
}
}
private InputElementWrapper? _inputElementWrapper;
public MainWindow()
{
@@ -45,12 +40,58 @@ namespace FileTime.Avalonia.Views
public async void OnKeyDown(object sender, KeyEventArgs e)
{
await ViewModel?.ProcessKeyDown(e.Key, e.KeyModifiers);
if (_inputElementWrapper == null)
{
e.Handled = e.Handled || await ViewModel?.ProcessKeyDown(e.Key, e.KeyModifiers);
}
}
public async void OnKeyUp(object sender, KeyEventArgs e)
{
await ViewModel?.ProcessKeyUp(e.Key, e.KeyModifiers);
if (_inputElementWrapper == null)
{
e.Handled = e.Handled || await ViewModel?.ProcessKeyUp(e.Key, e.KeyModifiers);
}
}
private void InputText_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && _inputElementWrapper == ViewModel!.Inputs.Last())
{
ViewModel.ProcessInputs();
_inputElementWrapper = null;
e.Handled = true;
}
else if (e.Key == Key.Escape && _inputElementWrapper == ViewModel!.Inputs.Last())
{
ViewModel.CancelInputs();
_inputElementWrapper = null;
e.Handled = true;
}
}
private void InputText_GotFocus(object sender, GotFocusEventArgs e)
{
if (sender is TextBox inputText && inputText.DataContext is InputElementWrapper inputElementWrapper)
{
_inputElementWrapper = inputElementWrapper;
}
}
private void InputText_LostFocus(object sender, RoutedEventArgs e)
{
if (sender is TextBox inputText && inputText.DataContext is InputElementWrapper inputElementWrapper)
{
_inputElementWrapper = null;
}
}
private void InputText_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs args)
{
if (sender is TextBox inputText && inputText.DataContext is InputElementWrapper inputElementWrapper && inputElementWrapper == ViewModel!.Inputs.First())
{
inputText.Focus();
}
}
}
}

View File

@@ -1,12 +1,12 @@
using FileTime.Core.Command;
using FileTime.Core.Models;
using FileTime.Core.StateManagement;
using FileTime.Core.Timeline;
namespace FileTime.Providers.Local.CommandHandlers
{
public class CopyCommandHandler : ICommandHandler
{
private readonly List<Thread> _copyOperations = new();
private readonly ElementCreationStates _elementCreationStates;
public CopyCommandHandler(ElementCreationStates elementCreationStates)
@@ -25,17 +25,14 @@ namespace FileTime.Providers.Local.CommandHandlers
return true;
}
public void Execute(object command)
public async Task ExecuteAsync(object command, TimeRunner timeRunner)
{
if (command is not CopyCommand copyCommand) throw new ArgumentException($"Can not execute command of type '{command.GetType()}'.");
var thread = new Thread(() => copyCommand.Execute(CopyElement));
thread.Start();
_copyOperations.Add(thread);
await copyCommand.Execute(CopyElement, timeRunner);
}
public void CopyElement(IAbsolutePath sourcePath, IAbsolutePath targetPath)
public static void CopyElement(AbsolutePath sourcePath, AbsolutePath targetPath)
{
using var sourceStream = File.OpenRead(sourcePath.Path);
using var sourceReader = new BinaryReader(sourceStream);
@@ -43,7 +40,7 @@ namespace FileTime.Providers.Local.CommandHandlers
using var targetStream = File.OpenWrite(targetPath.Path);
using var targetWriter = new BinaryWriter(targetStream);
var bufferSize = 1024 * 1024;
const int bufferSize = 1024 * 1024;
byte[] dataRead;
do

View File

@@ -1,3 +1,4 @@
using System;
using System.Runtime.InteropServices;
using AsyncEvent;
using FileTime.Core.Models;
@@ -19,12 +20,15 @@ namespace FileTime.Providers.Local
public string? FullName { get; }
public bool IsHidden => false;
public bool IsLoaded => true;
public IContentProvider Provider => this;
public AsyncEventHandler Refreshed { get; } = new();
public bool IsCaseInsensitive { get; }
public bool CanDelete => false;
public bool CanRename => false;
public LocalContentProvider(ILogger<LocalContentProvider> logger)
{
@@ -81,5 +85,7 @@ namespace FileTime.Providers.Local
public Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default) => Task.FromResult(_items);
public Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default) => Task.FromResult((IReadOnlyList<IContainer>?)_rootContainers);
public Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default) => Task.FromResult(_elements);
public Task Rename(string newName) => throw new NotSupportedException();
}
}

View File

@@ -24,13 +24,18 @@ namespace FileTime.Providers.Local
public string Attributes => GetAttributes();
public DateTime CreatedAt => File.CreationTime;
public bool CanDelete => true;
public bool CanRename => true;
public LocalFile(FileInfo file, IContentProvider contentProvider)
private readonly LocalFolder _parent;
public LocalFile(FileInfo file, LocalFolder parent, IContentProvider contentProvider)
{
_parent = parent;
File = file;
Name = file.Name;
FullName = file.FullName;
FullName = parent.FullName + Constants.SeparatorChar + file.Name;
Provider = contentProvider;
}
@@ -41,6 +46,14 @@ namespace FileTime.Providers.Local
File.Delete();
return Task.CompletedTask;
}
public async Task Rename(string newName)
{
if (_parent is LocalFolder parentFolder)
{
System.IO.File.Move(File.FullName, Path.Combine(parentFolder.Directory.FullName, newName));
await _parent.Refresh();
}
}
public string GetAttributes()
{
@@ -57,5 +70,7 @@ namespace FileTime.Providers.Local
+ ((File.Attributes & FileAttributes.System) == FileAttributes.System ? "s" : "-");
}
}
public IContainer? GetParent() => _parent;
}
}

View File

@@ -21,7 +21,12 @@ namespace FileTime.Providers.Local
public string FullName { get; }
public bool IsLoaded => _items != null;
public bool CanDelete => true;
public bool CanRename => true;
public AsyncEventHandler Refreshed { get; } = new();
public string Attributes => GetAttributes();
public DateTime CreatedAt => Directory.CreationTime;
@@ -48,7 +53,7 @@ namespace FileTime.Providers.Local
try
{
_containers = Directory.GetDirectories().Select(d => new LocalFolder(d, Provider, this)).OrderBy(d => d.Name).ToList().AsReadOnly();
_elements = Directory.GetFiles().Select(f => new LocalFile(f, Provider)).OrderBy(f => f.Name).ToList().AsReadOnly();
_elements = Directory.GetFiles().Select(f => new LocalFile(f, this, Provider)).OrderBy(f => f.Name).ToList().AsReadOnly();
}
catch { }
@@ -115,6 +120,14 @@ namespace FileTime.Providers.Local
Directory.Delete(true);
return Task.CompletedTask;
}
public async Task Rename(string newName)
{
if (_parent is LocalFolder parentFolder)
{
System.IO.Directory.Move(Directory.FullName, Path.Combine(parentFolder.Directory.FullName, newName));
await _parent.Refresh();
}
}
public string GetAttributes()
{

View File

@@ -1,3 +1,4 @@
using System;
using AsyncEvent;
using FileTime.Core.Interactions;
using FileTime.Core.Models;
@@ -11,7 +12,7 @@ namespace FileTime.Providers.Smb
private readonly IInputInterface _inputInterface;
private readonly List<IContainer> _rootContainers;
private readonly IReadOnlyList<IContainer> _rootContainersReadOnly;
private readonly IReadOnlyList<IItem>? _items;
private IReadOnlyList<IItem>? _items;
private readonly IReadOnlyList<IElement>? _elements = new List<IElement>().AsReadOnly();
public string Name { get; } = "smb";
@@ -19,14 +20,18 @@ namespace FileTime.Providers.Smb
public string? FullName { get; }
public bool IsHidden => false;
public bool IsLoaded => true;
public IContentProvider Provider => this;
public bool CanDelete => false;
public bool CanRename => false;
public AsyncEventHandler Refreshed { get; } = new();
public SmbContentProvider(IInputInterface inputInterface)
{
_rootContainers = new List<IContainer>();
_items = new List<IItem>();
_rootContainersReadOnly = _rootContainers.AsReadOnly();
_inputInterface = inputInterface;
}
@@ -40,6 +45,7 @@ namespace FileTime.Providers.Smb
{
container = new SmbServer(fullName, this, _inputInterface);
_rootContainers.Add(container);
_items = _rootContainers.OrderBy(c => c.Name).ToList().AsReadOnly();
}
await Refresh();
@@ -78,5 +84,7 @@ namespace FileTime.Providers.Smb
public Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default) => Task.FromResult(_items);
public Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default) => Task.FromResult((IReadOnlyList<IContainer>?)_rootContainersReadOnly);
public Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default) => Task.FromResult(_elements);
public Task Rename(string newName) => throw new NotSupportedException();
}
}

View File

@@ -13,8 +13,11 @@ namespace FileTime.Providers.Smb
public string? FullName { get; }
public bool IsHidden => false;
public bool CanDelete => true;
public bool CanRename => true;
public IContentProvider Provider { get; }
private IContainer _parent;
public SmbFile(string name, SmbContentProvider provider, IContainer parent)
{
@@ -22,16 +25,23 @@ namespace FileTime.Providers.Smb
FullName = parent.FullName + Constants.SeparatorChar + Name;
Provider = provider;
_parent = parent;
}
public Task Delete()
{
throw new NotImplementedException();
}
public Task Rename(string newName)
{
throw new NotImplementedException();
}
public string GetPrimaryAttributeText()
{
return "";
}
public IContainer? GetParent() => _parent;
}
}

View File

@@ -19,9 +19,12 @@ namespace FileTime.Providers.Smb
public string? FullName { get; }
public bool IsHidden => false;
public bool IsLoaded => _items != null;
public SmbContentProvider Provider { get; }
IContentProvider IItem.Provider => Provider;
public bool CanDelete => true;
public bool CanRename => true;
public AsyncEventHandler Refreshed { get; } = new();
@@ -77,6 +80,10 @@ namespace FileTime.Providers.Smb
{
throw new NotImplementedException();
}
public Task Rename(string newName)
{
throw new NotImplementedException();
}
public async Task Refresh()
{

View File

@@ -24,10 +24,13 @@ namespace FileTime.Providers.Smb
public string? FullName { get; }
public bool IsHidden => false;
public bool IsLoaded => _items != null;
public SmbContentProvider Provider { get; }
IContentProvider IItem.Provider => Provider;
public bool CanDelete => false;
public bool CanRename => false;
public AsyncEventHandler Refreshed { get; } = new();
@@ -128,5 +131,7 @@ namespace FileTime.Providers.Smb
}
return _client;
}
public Task Rename(string newName) => throw new NotSupportedException();
}
}

View File

@@ -1,5 +1,4 @@
using AsyncEvent;
using FileTime.Core.Interactions;
using FileTime.Core.Models;
using FileTime.Core.Providers;
using SMBLibrary;
@@ -20,9 +19,12 @@ namespace FileTime.Providers.Smb
public string? FullName { get; }
public bool IsHidden => false;
public bool IsLoaded => _items != null;
public SmbContentProvider Provider { get; }
IContentProvider IItem.Provider => Provider;
public bool CanDelete => false;
public bool CanRename => false;
public AsyncEventHandler Refreshed { get; } = new();
@@ -150,5 +152,7 @@ namespace FileTime.Providers.Smb
return (containers, elements);
}
public Task Rename(string newName) => throw new NotSupportedException();
}
}