From c2dcb4901654f72820ce1d704c30f4d40a6d9e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Mon, 31 Jan 2022 23:13:39 +0100 Subject: [PATCH] TimeTravel --- .../FileTime.App.Core/Clipboard/Clipboard.cs | 17 +- .../Clipboard/ClipboardItem.cs | 17 - .../FileTime.App.Core/Clipboard/IClipboard.cs | 8 +- .../FileTime.App.Core/Command/Commands.cs | 34 +- .../FileTime.App.Core/Tab/TabItem.cs | 17 - .../FileTime.App.Core/Tab/TabState.cs | 91 +++-- .../DependencyInjection.cs | 4 +- .../Application.CommandHandlers.cs | 55 +-- .../FileTime.ConsoleUI.App/Application.cs | 19 +- .../FileTime.ConsoleUI.App/UI/Render.cs | 4 +- src/Core/AsyncEvent/AsyncEventHandler.cs | 4 +- .../FileTime.Core/Command/CanCommandRun.cs | 9 + .../FileTime.Core/Command/CommandExecutor.cs | 8 +- src/Core/FileTime.Core/Command/CopyCommand.cs | 90 ++++- .../Command/CreateContainerCommand.cs | 45 ++- .../Command/CreateElementCommand.cs | 44 ++- .../FileTime.Core/Command/DeleteCommand.cs | 46 ++- src/Core/FileTime.Core/Command/ICommand.cs | 3 +- .../FileTime.Core/Command/ICommandHandler.cs | 4 +- .../Command/IExecutableCommand.cs | 4 +- .../Command/ITransportationCommand.cs | 6 +- src/Core/FileTime.Core/Command/MoveCommand.cs | 11 +- .../FileTime.Core/Command/RenameCommand.cs | 37 ++ src/Core/FileTime.Core/Components/Tab.cs | 68 +++- .../Extensions/TimelineExtensions.cs | 15 + .../Interactions/InputElement.cs | 4 +- .../FileTime.Core/Interactions/InputType.cs | 3 +- src/Core/FileTime.Core/Models/AbsolutePath.cs | 84 ++++- .../FileTime.Core/Models/IAbsolutePath.cs | 10 - src/Core/FileTime.Core/Models/IContainer.cs | 3 +- src/Core/FileTime.Core/Models/IItem.cs | 4 + .../FileTime.Core/Models/VirtualContainer.cs | 5 + .../FileTime.Core/Providers/TopContainer.cs | 6 + .../Timeline/CommandTimeState.cs | 27 ++ .../Timeline/ContainerSnapshot.cs | 7 - src/Core/FileTime.Core/Timeline/Difference.cs | 31 ++ .../Timeline/DifferenceActionType.cs | 8 + .../Timeline/DifferenceItemType.cs | 9 + .../FileTime.Core/Timeline/ElementSnapshot.cs | 0 .../Timeline/ParallelCommands.cs | 89 +++++ .../FileTime.Core/Timeline/PointInTime.cs | 36 +- .../Timeline/ReadOnlyCommandTimeState.cs | 18 + .../Timeline/ReadOnlyParallelCommands.cs | 11 + .../FileTime.Core/Timeline/RootSnapshot.cs | 7 - .../FileTime.Core/Timeline/TimeContainer.cs | 121 ++++++ .../FileTime.Core/Timeline/TimeElement.cs | 42 +++ .../FileTime.Core/Timeline/TimeProvider.cs | 89 +++++ src/Core/FileTime.Core/Timeline/TimeRunner.cs | 217 +++++++++++ src/GuiApp/FileTime.Avalonia/App.axaml | 45 ++- src/GuiApp/FileTime.Avalonia/App.axaml.cs | 1 + .../FileTime.Avalonia/Application/AppState.cs | 73 +++- .../Application/INewItemProcessor.cs | 10 + .../Application/TabContainer.cs | 164 +++++++-- .../Converters/IsNullConverter.cs | 23 ++ .../Converters/ItemToImageConverter.cs | 36 ++ .../ItemViewModeToBrushConverter.cs | 6 + .../IconProviders/IIconProvider.cs | 9 + .../IconProviders/MaterialIconProvider.cs | 33 ++ .../Misc/InputElementWrapper.cs | 3 +- .../Services/ItemNameConverterService.cs | 4 +- src/GuiApp/FileTime.Avalonia/Startup.cs | 10 + .../ViewModels/ContainerViewModel.cs | 106 ++++-- .../ViewModels/ElementViewModel.cs | 24 +- .../ViewModels/IItemViewModel.cs | 4 +- .../ViewModels/ItemViewMode.cs | 7 +- .../ViewModels/MainPageViewModel.cs | 343 +++++++++++++++++- .../FileTime.Avalonia/Views/ItemView.axaml | 2 +- .../FileTime.Avalonia/Views/MainWindow.axaml | 107 +++++- .../Views/MainWindow.axaml.cs | 65 +++- .../CommandHandlers/CopyCommandHandler.cs | 13 +- .../LocalContentProvider.cs | 6 + .../FileTime.Providers.Local/LocalFile.cs | 19 +- .../FileTime.Providers.Local/LocalFolder.cs | 15 +- .../SmbContentProvider.cs | 10 +- .../FileTime.Providers.Smb/SmbFile.cs | 10 + .../FileTime.Providers.Smb/SmbFolder.cs | 7 + .../FileTime.Providers.Smb/SmbServer.cs | 5 + .../FileTime.Providers.Smb/SmbShare.cs | 6 +- 78 files changed, 2294 insertions(+), 363 deletions(-) delete mode 100644 src/AppCommon/FileTime.App.Core/Clipboard/ClipboardItem.cs delete mode 100644 src/AppCommon/FileTime.App.Core/Tab/TabItem.cs create mode 100644 src/Core/FileTime.Core/Command/CanCommandRun.cs create mode 100644 src/Core/FileTime.Core/Command/RenameCommand.cs create mode 100644 src/Core/FileTime.Core/Extensions/TimelineExtensions.cs delete mode 100644 src/Core/FileTime.Core/Models/IAbsolutePath.cs create mode 100644 src/Core/FileTime.Core/Timeline/CommandTimeState.cs delete mode 100644 src/Core/FileTime.Core/Timeline/ContainerSnapshot.cs create mode 100644 src/Core/FileTime.Core/Timeline/Difference.cs create mode 100644 src/Core/FileTime.Core/Timeline/DifferenceActionType.cs create mode 100644 src/Core/FileTime.Core/Timeline/DifferenceItemType.cs delete mode 100644 src/Core/FileTime.Core/Timeline/ElementSnapshot.cs create mode 100644 src/Core/FileTime.Core/Timeline/ParallelCommands.cs create mode 100644 src/Core/FileTime.Core/Timeline/ReadOnlyCommandTimeState.cs create mode 100644 src/Core/FileTime.Core/Timeline/ReadOnlyParallelCommands.cs delete mode 100644 src/Core/FileTime.Core/Timeline/RootSnapshot.cs create mode 100644 src/Core/FileTime.Core/Timeline/TimeContainer.cs create mode 100644 src/Core/FileTime.Core/Timeline/TimeElement.cs create mode 100644 src/Core/FileTime.Core/Timeline/TimeProvider.cs create mode 100644 src/Core/FileTime.Core/Timeline/TimeRunner.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Application/INewItemProcessor.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Converters/IsNullConverter.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Converters/ItemToImageConverter.cs create mode 100644 src/GuiApp/FileTime.Avalonia/IconProviders/IIconProvider.cs create mode 100644 src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs diff --git a/src/AppCommon/FileTime.App.Core/Clipboard/Clipboard.cs b/src/AppCommon/FileTime.App.Core/Clipboard/Clipboard.cs index bad4665..e1e6e23 100644 --- a/src/AppCommon/FileTime.App.Core/Clipboard/Clipboard.cs +++ b/src/AppCommon/FileTime.App.Core/Clipboard/Clipboard.cs @@ -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 _content; - public IReadOnlyList Content { get; } + private readonly List _content; + public IReadOnlyList Content { get; } public Type? CommandType { get; private set; } public Clipboard() { - _content = new List(); + _content = new List(); 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--); } diff --git a/src/AppCommon/FileTime.App.Core/Clipboard/ClipboardItem.cs b/src/AppCommon/FileTime.App.Core/Clipboard/ClipboardItem.cs deleted file mode 100644 index 0ddd07a..0000000 --- a/src/AppCommon/FileTime.App.Core/Clipboard/ClipboardItem.cs +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Clipboard/IClipboard.cs b/src/AppCommon/FileTime.App.Core/Clipboard/IClipboard.cs index 96b09ee..76dc16a 100644 --- a/src/AppCommon/FileTime.App.Core/Clipboard/IClipboard.cs +++ b/src/AppCommon/FileTime.App.Core/Clipboard/IClipboard.cs @@ -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 Content { get; } + IReadOnlyList 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() where T : ITransportationCommand; } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Command/Commands.cs b/src/AppCommon/FileTime.App.Core/Command/Commands.cs index 8f83c72..f12fff2 100644 --- a/src/AppCommon/FileTime.App.Core/Command/Commands.cs +++ b/src/AppCommon/FileTime.App.Core/Command/Commands.cs @@ -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, } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Tab/TabItem.cs b/src/AppCommon/FileTime.App.Core/Tab/TabItem.cs deleted file mode 100644 index b98cda9..0000000 --- a/src/AppCommon/FileTime.App.Core/Tab/TabItem.cs +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Tab/TabState.cs b/src/AppCommon/FileTime.App.Core/Tab/TabState.cs index 2d4ca96..c7056de 100644 --- a/src/AppCommon/FileTime.App.Core/Tab/TabState.cs +++ b/src/AppCommon/FileTime.App.Core/Tab/TabState.cs @@ -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> _selectedItems; - private readonly Dictionary> _selectedItemsReadOnly; - public IReadOnlyDictionary> SelectedItems { get; } + private readonly Dictionary> _markedItems; + private readonly Dictionary> _markedItemsReadOnly; + public IReadOnlyDictionary> MarkedItems { get; } public FileTime.Core.Components.Tab Tab { get; } + public AsyncEventHandler ItemMarked { get; } = new(); + public AsyncEventHandler ItemUnmarked { get; } = new(); + public TabState(FileTime.Core.Components.Tab pane) { Tab = pane; - _selectedItems = new Dictionary>(); - _selectedItemsReadOnly = new Dictionary>(); - SelectedItems = new ReadOnlyDictionary>(_selectedItemsReadOnly); + _markedItems = new Dictionary>(); + _markedItemsReadOnly = new Dictionary>(); + MarkedItems = new ReadOnlyDictionary>(_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(); - _selectedItems.Add(container, val); - _selectedItemsReadOnly.Add(container, val.AsReadOnly()); + var val = new List(); + _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> GetCurrentSelectedItems() + public async Task> GetCurrentMarkedItems() { - var currentLocation = await Tab.GetCurrentLocation(); + return GetCurrentMarkedItems(await Tab.GetCurrentLocation()); + } - return SelectedItems.ContainsKey(currentLocation) - ? SelectedItems[currentLocation] - : new List().AsReadOnly(); + public IReadOnlyList GetCurrentMarkedItems(IContainer container) + { + return MarkedItems.ContainsKey(container) + ? MarkedItems[container] + : new List().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(); + } } } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs index 69f09c9..024e62a 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs +++ b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs @@ -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(sp => sp.GetService() ?? throw new Exception($"No {nameof(LocalContentProvider)} instance found")) .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); } } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs index 4c9ed99..8cf77fe 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs @@ -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 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(); - 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? itemsToDelete = null; + IList? 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().ToList(); + itemsToDelete = currentSelectedItems.Cast().ToList(); } } else if (currentSelectedItem != null) @@ -225,9 +198,9 @@ namespace FileTime.ConsoleUI.App if (delete) { - itemsToDelete = new List() + itemsToDelete = new List() { - 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(); } diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Application.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Application.cs index 287e016..dcccbd9 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Application.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Application.cs @@ -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 _tabs = new(); private readonly Dictionary _renderers = new(); - private readonly Dictionary _paneStates = new(); + private readonly Dictionary _tabStates = new(); private Tab? _selectedTab; private readonly List _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 _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()!; 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), diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/UI/Render.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/UI/Render.cs index 9170c3a..ff43d39 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/UI/Render.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/UI/Render.cs @@ -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; diff --git a/src/Core/AsyncEvent/AsyncEventHandler.cs b/src/Core/AsyncEvent/AsyncEventHandler.cs index bb05bd8..2064a37 100644 --- a/src/Core/AsyncEvent/AsyncEventHandler.cs +++ b/src/Core/AsyncEvent/AsyncEventHandler.cs @@ -1,6 +1,6 @@ namespace AsyncEvent { - public class AsyncEventHandler where TArg : AsyncEventArgs + public class AsyncEventHandler { private readonly List> _handlers; private readonly Action> _add; @@ -56,7 +56,7 @@ return obj; } } - public class AsyncEventHandler : AsyncEventHandler where TArg : AsyncEventArgs + public class AsyncEventHandler : AsyncEventHandler { public AsyncEventHandler(Action>? add = null, Action>? remove = null) : base(add, remove) { } } diff --git a/src/Core/FileTime.Core/Command/CanCommandRun.cs b/src/Core/FileTime.Core/Command/CanCommandRun.cs new file mode 100644 index 0000000..17b76bb --- /dev/null +++ b/src/Core/FileTime.Core/Command/CanCommandRun.cs @@ -0,0 +1,9 @@ +namespace FileTime.Core.Command +{ + public enum CanCommandRun + { + True, + False, + Forceable + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Command/CommandExecutor.cs b/src/Core/FileTime.Core/Command/CommandExecutor.cs index a62dd52..2488148 100644 --- a/src/Core/FileTime.Core/Command/CommandExecutor.cs +++ b/src/Core/FileTime.Core/Command/CommandExecutor.cs @@ -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); } } } diff --git a/src/Core/FileTime.Core/Command/CopyCommand.cs b/src/Core/FileTime.Core/Command/CopyCommand.cs index fc88879..bf84a5a 100644 --- a/src/Core/FileTime.Core/Command/CopyCommand.cs +++ b/src/Core/FileTime.Core/Command/CopyCommand.cs @@ -5,59 +5,115 @@ namespace FileTime.Core.Command { public class CopyCommand : ITransportationCommand { - public IList Sources { get; } = new List(); + private Action? _copyOperation; + private Func>? _createContainer; + private TimeRunner? _timeRunner; + + public IList? Sources { get; } = new List(); 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 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(); + + _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 copy) + public async Task Execute(Action 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 sources, IContainer target, TransportMode transportMode, Action copy) + private async Task DoCopy( + IEnumerable sources, + IContainer target, + TransportMode transportMode) { + if (_copyOperation == null) throw new ArgumentException("No copy operation were given."); + if (_createContainer == null) throw new ArgumentException("No container creation function were given."); + foreach (var source in sources) { - var item = await source.ContentProvider.GetByPath(source.Path); + var item = await source.Resolve(); if (item is IContainer container) { - var targetContainer = (await target.GetContainers())?.FirstOrDefault(d => d.Name == container.Name) ?? (await target.CreateContainer(container.Name)!); + var targetContainer = (await target.GetContainers())?.FirstOrDefault(d => d.Name == container.Name) ?? (await _createContainer?.Invoke(target, container.Name)!); - var childDirectories = (await container.GetContainers())!.Select(d => new AbsolutePath(item.Provider, d.FullName!)); - var childFiles = (await container.GetElements())!.Select(f => new AbsolutePath(item.Provider, f.FullName!)); + var childDirectories = (await container.GetContainers())!.Select(d => new AbsolutePath(d)); + var childFiles = (await container.GetElements())!.Select(f => new AbsolutePath(f)); - await DoCopy(childDirectories.Concat(childFiles), targetContainer, transportMode, copy); + await DoCopy(childDirectories.Concat(childFiles), targetContainer, transportMode); + _timeRunner?.RefreshContainer.InvokeAsync(this, new AbsolutePath(container)); } else if (item is IElement element) { var targetName = element.Name; var targetNameExists = await target.IsExists(targetName); - if (transportMode == TransportMode.Merge) + if (transportMode == Command.TransportMode.Merge) { for (var i = 0; targetNameExists; i++) { targetName = element.Name + (i == 0 ? "_" : $"_{i}"); } } - else if (transportMode == TransportMode.Skip && targetNameExists) + else if (transportMode == Command.TransportMode.Skip && targetNameExists) { continue; } - var targetPath = target.FullName + Constants.SeparatorChar + targetName; - - copy(new AbsolutePath(source.ContentProvider, element.FullName!), new AbsolutePath(target.Provider, targetPath)); + _copyOperation?.Invoke(new AbsolutePath(element), AbsolutePath.FromParentAndChildName(target, targetName)); } } } + + public Task CanRun(PointInTime startPoint) + { + //TODO: implement + return Task.FromResult(CanCommandRun.True); + } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Command/CreateContainerCommand.cs b/src/Core/FileTime.Core/Command/CreateContainerCommand.cs index 90a2f48..b847a17 100644 --- a/src/Core/FileTime.Core/Command/CreateContainerCommand.cs +++ b/src/Core/FileTime.Core/Command/CreateContainerCommand.cs @@ -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 SimulateCommand(PointInTime startPoint) + { + var newDifferences = new List() + { + 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 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; } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Command/CreateElementCommand.cs b/src/Core/FileTime.Core/Command/CreateElementCommand.cs index e851181..38e081c 100644 --- a/src/Core/FileTime.Core/Command/CreateElementCommand.cs +++ b/src/Core/FileTime.Core/Command/CreateElementCommand.cs @@ -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 SimulateCommand(PointInTime startPoint) + { + var newDifferences = new List() + { + 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 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; } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Command/DeleteCommand.cs b/src/Core/FileTime.Core/Command/DeleteCommand.cs index c8ceba1..ec4e943 100644 --- a/src/Core/FileTime.Core/Command/DeleteCommand.cs +++ b/src/Core/FileTime.Core/Command/DeleteCommand.cs @@ -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 ItemsToDelete { get; } = new List(); + public IList ItemsToDelete { get; } = new List(); - public PointInTime SimulateCommand(PointInTime delta) + public async Task SimulateCommand(PointInTime startPoint) { - throw new NotImplementedException(); + var newDifferences = new List(); + + 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 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; + } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Command/ICommand.cs b/src/Core/FileTime.Core/Command/ICommand.cs index 355fc54..0e0fd54 100644 --- a/src/Core/FileTime.Core/Command/ICommand.cs +++ b/src/Core/FileTime.Core/Command/ICommand.cs @@ -4,6 +4,7 @@ namespace FileTime.Core.Command { public interface ICommand { - PointInTime SimulateCommand(PointInTime moment); + Task CanRun(PointInTime startPoint); + Task SimulateCommand(PointInTime startPoint); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Command/ICommandHandler.cs b/src/Core/FileTime.Core/Command/ICommandHandler.cs index 74dc6ef..d4170ef 100644 --- a/src/Core/FileTime.Core/Command/ICommandHandler.cs +++ b/src/Core/FileTime.Core/Command/ICommandHandler.cs @@ -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); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Command/IExecutableCommand.cs b/src/Core/FileTime.Core/Command/IExecutableCommand.cs index 05eef55..0024618 100644 --- a/src/Core/FileTime.Core/Command/IExecutableCommand.cs +++ b/src/Core/FileTime.Core/Command/IExecutableCommand.cs @@ -1,7 +1,9 @@ +using FileTime.Core.Timeline; + namespace FileTime.Core.Command { public interface IExecutableCommand : ICommand { - Task Execute(); + Task Execute(TimeRunner timeRunner); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Command/ITransportationCommand.cs b/src/Core/FileTime.Core/Command/ITransportationCommand.cs index f065c12..61ac5c1 100644 --- a/src/Core/FileTime.Core/Command/ITransportationCommand.cs +++ b/src/Core/FileTime.Core/Command/ITransportationCommand.cs @@ -4,8 +4,8 @@ namespace FileTime.Core.Command { public interface ITransportationCommand : ICommand { - IList Sources { get; } - IContainer Target { get; set;} - TransportMode TransportMode { get; set; } + IList? Sources { get; } + IContainer? Target { get; set;} + TransportMode? TransportMode { get; set; } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Command/MoveCommand.cs b/src/Core/FileTime.Core/Command/MoveCommand.cs index 2b6f2a4..2b85fdb 100644 --- a/src/Core/FileTime.Core/Command/MoveCommand.cs +++ b/src/Core/FileTime.Core/Command/MoveCommand.cs @@ -5,12 +5,17 @@ namespace FileTime.Core.Command { public class MoveCommand : ITransportationCommand { - public IList Sources { get; } = new List(); + public IList? Sources { get; } = new List(); 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 CanRun(PointInTime startPoint) + { + throw new NotImplementedException(); + } + + public Task SimulateCommand(PointInTime startPoint) { throw new NotImplementedException(); } diff --git a/src/Core/FileTime.Core/Command/RenameCommand.cs b/src/Core/FileTime.Core/Command/RenameCommand.cs new file mode 100644 index 0000000..28d8aa5 --- /dev/null +++ b/src/Core/FileTime.Core/Command/RenameCommand.cs @@ -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 SimulateCommand(PointInTime startPoint) + { + throw new NotImplementedException(); + } + + public Task CanRun(PointInTime startPoint) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Components/Tab.cs b/src/Core/FileTime.Core/Components/Tab.cs index 8d155e9..b2b8c6b 100644 --- a/src/Core/FileTime.Core/Components/Tab.cs +++ b/src/Core/FileTime.Core/Components/Tab.cs @@ -7,7 +7,8 @@ namespace FileTime.Core.Components { private IItem? _currentSelectedItem; private IContainer _currentLocation; - + private string? _lastPath; + public int CurrentSelectedIndex { get; private set; } public AsyncEventHandler CurrentLocationChanged = new(); @@ -33,10 +34,10 @@ namespace FileTime.Core.Components } _currentLocation = value; - await CurrentLocationChanged?.InvokeAsync(this, AsyncEventArgs.Empty); + await CurrentLocationChanged.InvokeAsync(this, AsyncEventArgs.Empty); var currentLocationItems = (await (await GetCurrentLocation()).GetItems())!; - await SetCurrentSelectedItem(currentLocationItems.Count > 0 ? currentLocationItems[0] : null); + await SetCurrentSelectedItem(await GetItemByLastPath() ?? (currentLocationItems.Count > 0 ? currentLocationItems[0] : null)); _currentLocation.Refreshed.Add(HandleCurrentLocationRefresh); } } @@ -58,14 +59,71 @@ namespace FileTime.Core.Components } _currentSelectedItem = itemToSelect; + _lastPath = GetCommonPath(_lastPath, itemToSelect?.FullName); CurrentSelectedIndex = await GetItemIndex(itemToSelect); - await CurrentSelectedItemChanged?.InvokeAsync(this, AsyncEventArgs.Empty); + await CurrentSelectedItemChanged.InvokeAsync(this, AsyncEventArgs.Empty); } } + public async Task 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(); + + 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) { diff --git a/src/Core/FileTime.Core/Extensions/TimelineExtensions.cs b/src/Core/FileTime.Core/Extensions/TimelineExtensions.cs new file mode 100644 index 0000000..2ad9a08 --- /dev/null +++ b/src/Core/FileTime.Core/Extensions/TimelineExtensions.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Interactions/InputElement.cs b/src/Core/FileTime.Core/Interactions/InputElement.cs index 2ac37f5..87be4c1 100644 --- a/src/Core/FileTime.Core/Interactions/InputElement.cs +++ b/src/Core/FileTime.Core/Interactions/InputElement.cs @@ -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; } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Interactions/InputType.cs b/src/Core/FileTime.Core/Interactions/InputType.cs index bc2342b..6eee3d4 100644 --- a/src/Core/FileTime.Core/Interactions/InputType.cs +++ b/src/Core/FileTime.Core/Interactions/InputType.cs @@ -3,6 +3,7 @@ namespace FileTime.Core.Interactions public enum InputType { Text, - Password + Password, + Bool } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Models/AbsolutePath.cs b/src/Core/FileTime.Core/Models/AbsolutePath.cs index 7b6ae2e..929eed2 100644 --- a/src/Core/FileTime.Core/Models/AbsolutePath.cs +++ b/src/Core/FileTime.Core/Models/AbsolutePath.cs @@ -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 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(); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Models/IAbsolutePath.cs b/src/Core/FileTime.Core/Models/IAbsolutePath.cs deleted file mode 100644 index fc88672..0000000 --- a/src/Core/FileTime.Core/Models/IAbsolutePath.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FileTime.Core.Providers; - -namespace FileTime.Core.Models -{ - public interface IAbsolutePath - { - IContentProvider ContentProvider { get; } - string Path { get; } - } -} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Models/IContainer.cs b/src/Core/FileTime.Core/Models/IContainer.cs index f6a70f8..d19c139 100644 --- a/src/Core/FileTime.Core/Models/IContainer.cs +++ b/src/Core/FileTime.Core/Models/IContainer.cs @@ -9,7 +9,6 @@ namespace FileTime.Core.Models Task?> GetElements(CancellationToken token = default); Task Refresh(); - IContainer? GetParent(); Task GetByPath(string path); Task CreateContainer(string name); Task CreateElement(string name); @@ -18,6 +17,8 @@ namespace FileTime.Core.Models Task Clone(); + bool IsLoaded { get; } + AsyncEventHandler Refreshed { get; } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Models/IItem.cs b/src/Core/FileTime.Core/Models/IItem.cs index 68de20d..bd3cadd 100644 --- a/src/Core/FileTime.Core/Models/IItem.cs +++ b/src/Core/FileTime.Core/Models/IItem.cs @@ -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(); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Models/VirtualContainer.cs b/src/Core/FileTime.Core/Models/VirtualContainer.cs index 931e5eb..e818373 100644 --- a/src/Core/FileTime.Core/Models/VirtualContainer.cs +++ b/src/Core/FileTime.Core/Models/VirtualContainer.cs @@ -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); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Providers/TopContainer.cs b/src/Core/FileTime.Core/Providers/TopContainer.cs index 9615d04..5430349 100644 --- a/src/Core/FileTime.Core/Providers/TopContainer.cs +++ b/src/Core/FileTime.Core/Providers/TopContainer.cs @@ -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?> GetElements(CancellationToken token = default) => Task.FromResult(_elements); public Task Clone() => Task.FromResult((IContainer)this); + + public Task Rename(string newName) => throw new NotSupportedException(); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/CommandTimeState.cs b/src/Core/FileTime.Core/Timeline/CommandTimeState.cs new file mode 100644 index 0000000..1763e01 --- /dev/null +++ b/src/Core/FileTime.Core/Timeline/CommandTimeState.cs @@ -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; + } + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/ContainerSnapshot.cs b/src/Core/FileTime.Core/Timeline/ContainerSnapshot.cs deleted file mode 100644 index 32685e9..0000000 --- a/src/Core/FileTime.Core/Timeline/ContainerSnapshot.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FileTime.Core.Timeline -{ - public class ContainerSnapshot - { - - } -} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/Difference.cs b/src/Core/FileTime.Core/Timeline/Difference.cs new file mode 100644 index 0000000..f96a1e4 --- /dev/null +++ b/src/Core/FileTime.Core/Timeline/Difference.cs @@ -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) + ); + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/DifferenceActionType.cs b/src/Core/FileTime.Core/Timeline/DifferenceActionType.cs new file mode 100644 index 0000000..9d72470 --- /dev/null +++ b/src/Core/FileTime.Core/Timeline/DifferenceActionType.cs @@ -0,0 +1,8 @@ +namespace FileTime.Core.Timeline +{ + public enum DifferenceActionType + { + Create, + Delete + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/DifferenceItemType.cs b/src/Core/FileTime.Core/Timeline/DifferenceItemType.cs new file mode 100644 index 0000000..e75b4a3 --- /dev/null +++ b/src/Core/FileTime.Core/Timeline/DifferenceItemType.cs @@ -0,0 +1,9 @@ +namespace FileTime.Core.Timeline +{ + public enum DifferenceItemType + { + Container, + Element, + Unknown + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/ElementSnapshot.cs b/src/Core/FileTime.Core/Timeline/ElementSnapshot.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/Core/FileTime.Core/Timeline/ParallelCommands.cs b/src/Core/FileTime.Core/Timeline/ParallelCommands.cs new file mode 100644 index 0000000..861d6e2 --- /dev/null +++ b/src/Core/FileTime.Core/Timeline/ParallelCommands.cs @@ -0,0 +1,89 @@ +using FileTime.Core.Command; + +namespace FileTime.Core.Timeline +{ + public class ParallelCommands + { + private static ushort _idCounter; + public List _commands; + public ushort Id { get; } + public IReadOnlyList Commands { get; } + public PointInTime? Result { get; private set; } + + public ParallelCommands(PointInTime? result) + : this(new List(), result) { } + + private ParallelCommands(List commands, PointInTime? result) + { + Id = _idCounter++; + + _commands = commands; + Commands = _commands.AsReadOnly(); + + Result = result; + } + + public static async Task Create(PointInTime? startTime, IEnumerable commands) + { + var commandStates = new List(); + 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 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); + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/PointInTime.cs b/src/Core/FileTime.Core/Timeline/PointInTime.cs index 61aacd8..eab1ca8 100644 --- a/src/Core/FileTime.Core/Timeline/PointInTime.cs +++ b/src/Core/FileTime.Core/Timeline/PointInTime.cs @@ -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 snapshots = new(); + private readonly List _differences; - public IReadOnlyDictionary Snapshots => new Lazy>(() => new ReadOnlyDictionary(snapshots)).Value; + public IReadOnlyList Differences { get; } + + public IContentProvider? Provider { get; } + + private PointInTime() : this(new List(), null) { } + + private PointInTime(IEnumerable differences, IContentProvider? provider) + { + _differences = new List(differences); + Differences = _differences.AsReadOnly(); + Provider = provider; + } + + private PointInTime(PointInTime previous, IEnumerable differences, IContentProvider provider) + : this(MergeDifferences(previous.Differences, differences, provider), provider) { } + + public PointInTime WithDifferences(IEnumerable differences) => new(this, differences, new TimeProvider(this)); + + private static List MergeDifferences(IEnumerable previouses, IEnumerable differences, IContentProvider virtualProvider) + { + var merged = new List(); + + 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(), parentProvder); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/ReadOnlyCommandTimeState.cs b/src/Core/FileTime.Core/Timeline/ReadOnlyCommandTimeState.cs new file mode 100644 index 0000000..e678a41 --- /dev/null +++ b/src/Core/FileTime.Core/Timeline/ReadOnlyCommandTimeState.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/ReadOnlyParallelCommands.cs b/src/Core/FileTime.Core/Timeline/ReadOnlyParallelCommands.cs new file mode 100644 index 0000000..5bd22eb --- /dev/null +++ b/src/Core/FileTime.Core/Timeline/ReadOnlyParallelCommands.cs @@ -0,0 +1,11 @@ +namespace FileTime.Core.Timeline +{ + public class ReadOnlyParallelCommands + { + public IReadOnlyList Commands { get; } + public ReadOnlyParallelCommands(ParallelCommands parallelCommands) + { + Commands = parallelCommands.Commands.Select(c => new ReadOnlyCommandTimeState(c)).ToList().AsReadOnly(); + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/RootSnapshot.cs b/src/Core/FileTime.Core/Timeline/RootSnapshot.cs deleted file mode 100644 index 46a0dae..0000000 --- a/src/Core/FileTime.Core/Timeline/RootSnapshot.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FileTime.Core.Timeline -{ - public class RootSnapshot - { - - } -} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/TimeContainer.cs b/src/Core/FileTime.Core/Timeline/TimeContainer.cs new file mode 100644 index 0000000..2165616 --- /dev/null +++ b/src/Core/FileTime.Core/Timeline/TimeContainer.cs @@ -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 Clone() => new TimeContainer(Name, await _parent!.Clone(), Provider, VirtualProvider, _pointInTime); + + public Task CreateContainer(string name) => Task.FromResult((IContainer)new TimeContainer(name, this, Provider, VirtualProvider, _pointInTime)); + + public Task CreateElement(string name) => Task.FromResult((IElement)new TimeElement(name, this, Provider, VirtualProvider)); + + public Task Delete() => Task.CompletedTask; + + public async Task 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?> GetContainers(CancellationToken token = default) => + Task.FromResult( + (IReadOnlyList?)_pointInTime + .Differences + .Where(d => + d.Type == DifferenceItemType.Container + && GetParentPath(d.AbsolutePath.Path) == FullName) + .Select(MapContainer) + .ToList() + .AsReadOnly() + ); + + public Task?> GetElements(CancellationToken token = default) => + Task.FromResult( + (IReadOnlyList?)_pointInTime + .Differences + .Where(d => + d.Type == DifferenceItemType.Element + && GetParentPath(d.AbsolutePath.Path) == FullName) + .Select(MapElement) + .ToList() + .AsReadOnly() + ); + + public async Task?> GetItems(CancellationToken token = default) + { + var containers = (await GetContainers(token))!; + var elements = (await GetElements(token))!; + + return containers.Cast().Concat(elements).ToList().AsReadOnly(); + } + + public IContainer? GetParent() => _parent; + + public async Task 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); + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/TimeElement.cs b/src/Core/FileTime.Core/Timeline/TimeElement.cs new file mode 100644 index 0000000..1c90bda --- /dev/null +++ b/src/Core/FileTime.Core/Timeline/TimeElement.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/TimeProvider.cs b/src/Core/FileTime.Core/Timeline/TimeProvider.cs new file mode 100644 index 0000000..229cb50 --- /dev/null +++ b/src/Core/FileTime.Core/Timeline/TimeProvider.cs @@ -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 Clone() => Task.FromResult((IContainer)this); + + public Task CreateContainer(string name) + { + throw new NotImplementedException(); + } + + public Task CreateElement(string name) + { + throw new NotImplementedException(); + } + + public Task Delete() => throw new NotSupportedException(); + + public Task GetByPath(string path) + { + throw new NotImplementedException(); + } + + public Task?> GetContainers(CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public Task?> GetElements(CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public Task?> GetItems(CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public IContainer? GetParent() => null; + + public Task> GetRootContainers(CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public Task IsExists(string name) + { + throw new NotImplementedException(); + } + + public Task Refresh() => Task.CompletedTask; + + public Task Rename(string newName) => throw new NotSupportedException(); + + public void SetParent(IContainer container) { } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/TimeRunner.cs b/src/Core/FileTime.Core/Timeline/TimeRunner.cs new file mode 100644 index 0000000..9687942 --- /dev/null +++ b/src/Core/FileTime.Core/Timeline/TimeRunner.cs @@ -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 _commandsToRun = new(); + private readonly object _guard = new(); + + private bool _resourceIsInUse; + private readonly List _commandRunners = new(); + private bool _enableRunning = true; + + public bool EnableRunning + { + get + { + bool result = true; + RunWithLock(() => result = _enableRunning); + return result; + } + + set + { + RunWithLock(() => _enableRunning = value); + } + } + + public IReadOnlyList ParallelCommands { get; private set; } = new List().AsReadOnly(); + + public AsyncEventHandler RefreshContainer { get; } = new AsyncEventHandler(); + + 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 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(); + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/App.axaml b/src/GuiApp/FileTime.Avalonia/App.axaml index 4720bdb..202ae03 100644 --- a/src/GuiApp/FileTime.Avalonia/App.axaml +++ b/src/GuiApp/FileTime.Avalonia/App.axaml @@ -15,12 +15,18 @@ #00000000 #10000000 #93a1a1 + #00000000 + #10000000 + #b58900 #93a1a1 #268bd2 #7793a1a1 - #93a1a1 + #93a1a1 #073642 + #b58900 + #b58900 + #002b36 #dc322f @@ -48,6 +54,15 @@ + + + + x:Key="AlternativeItemForegroundBrush" + Color="{DynamicResource AlternativeItemForegroundColor}" /> + + + + AlternativeBrush="{StaticResource AlternativeItemForegroundBrush}" + SelectedBrush="{StaticResource SelectedItemForegroundBrush}" + MarkedBrush="{StaticResource MarkedItemForegroundBrush}" + MarkedAlternativeBrush="{StaticResource MarkedAlternativeItemForegroundBrush}" + MarkedSelectedBrush="{StaticResource MarkedSelectedItemForegroundBrush}"/> + SelectedBrush="{StaticResource SelectedItemBackgroundBrush}" + MarkedBrush="{StaticResource MarkedItemBackgroundBrush}" + MarkedAlternativeBrush="{StaticResource MarkedAlternativeItemBackgroundBrush}" + MarkedSelectedBrush="{StaticResource MarkedSelectedItemBackgroundBrush}"/> + + + diff --git a/src/GuiApp/FileTime.Avalonia/App.axaml.cs b/src/GuiApp/FileTime.Avalonia/App.axaml.cs index 7fc7f03..aab6321 100644 --- a/src/GuiApp/FileTime.Avalonia/App.axaml.cs +++ b/src/GuiApp/FileTime.Avalonia/App.axaml.cs @@ -19,6 +19,7 @@ namespace FileTime.Avalonia .RegisterDefaultServices() .AddViewModels() .AddServices() + .RegisterCommandHandlers() .BuildServiceProvider(); } diff --git a/src/GuiApp/FileTime.Avalonia/Application/AppState.cs b/src/GuiApp/FileTime.Avalonia/Application/AppState.cs index bd1b6e8..9485dee 100644 --- a/src/GuiApp/FileTime.Avalonia/Application/AppState.cs +++ b/src/GuiApp/FileTime.Avalonia/Application/AppState.cs @@ -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 _tabs = new ObservableCollection(); + private ObservableCollection _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 itemsAdded = new(); + List itemsRemoved = new(); + if (e.NewItems != null && e.OldItems != null) + { + itemsAdded.AddRange(e.NewItems.Cast().Except(e.OldItems.Cast())); + itemsRemoved.AddRange(e.OldItems.Cast().Except(e.NewItems.Cast())); + } + else if (e.NewItems != null) + { + itemsAdded.AddRange(e.NewItems.Cast()); + } + else if (e.OldItems != null) + { + itemsRemoved.AddRange(e.OldItems.Cast()); + } + + 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; + } + } } } } diff --git a/src/GuiApp/FileTime.Avalonia/Application/INewItemProcessor.cs b/src/GuiApp/FileTime.Avalonia/Application/INewItemProcessor.cs new file mode 100644 index 0000000..e972a9d --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Application/INewItemProcessor.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using FileTime.Avalonia.ViewModels; + +namespace FileTime.Avalonia.Application +{ + public interface INewItemProcessor + { + Task UpdateMarkedItems(ContainerViewModel containerViewModel); + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs index 61eff10..ba06d62 100644 --- a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs +++ b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs @@ -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,39 +120,89 @@ namespace FileTime.Avalonia.Application await UpdateCurrentSelectedItem(); } - private async Task UpdateCurrentSelectedItem() + public async Task UpdateCurrentSelectedItem() { - var tabCurrentSelectenItem = await Tab.GetCurrentSelectedItem(); - IItemViewModel? currentSelectenItem = null; - if (tabCurrentSelectenItem == null) + try { - SelectedItem = null; - ChildContainer = null; - } - else - { - currentSelectenItem = (await _currentLocation.GetItems()).FirstOrDefault(i => i.Item.Name == tabCurrentSelectenItem.Name); - if (currentSelectenItem is ContainerViewModel currentSelectedContainer) - { - SelectedItem = currentSelectedContainer; - ChildContainer = currentSelectedContainer; - } - else if (currentSelectenItem is ElementViewModel element) - { - SelectedItem = element; - ChildContainer = null; - } - else + var tabCurrentSelectenItem = await Tab.GetCurrentSelectedItem(); + IItemViewModel? currentSelectenItem = null; + if (tabCurrentSelectenItem == null) { SelectedItem = null; ChildContainer = null; } - } + else + { + currentSelectenItem = (await _currentLocation.GetItems()).FirstOrDefault(i => i.Item.Name == tabCurrentSelectenItem.Name); + if (currentSelectenItem is ContainerViewModel currentSelectedContainer) + { + SelectedItem = currentSelectedContainer; + ChildContainer = currentSelectedContainer; + } + else if (currentSelectenItem is ElementViewModel element) + { + SelectedItem = element; + ChildContainer = null; + } + else + { + SelectedItem = null; + ChildContainer = null; + } + } - var items = await _currentLocation.GetItems(); - foreach (var item in items) + var items = await _currentLocation.GetItems(); + if (items != null && items.Count > 0) + { + foreach (var item in items) + { + 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 { - item.IsSelected = item == currentSelectenItem; + //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); + } + } + } } } diff --git a/src/GuiApp/FileTime.Avalonia/Converters/IsNullConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/IsNullConverter.cs new file mode 100644 index 0000000..0c233f7 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Converters/IsNullConverter.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Converters/ItemToImageConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/ItemToImageConverter.cs new file mode 100644 index 0000000..6342d3f --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Converters/ItemToImageConverter.cs @@ -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("avares://FileTime.Avalonia" + path, null); + return new SvgImage { Source = source }; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Converters/ItemViewModeToBrushConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/ItemViewModeToBrushConverter.cs index 4eed0c7..9934e5d 100644 --- a/src/GuiApp/FileTime.Avalonia/Converters/ItemViewModeToBrushConverter.cs +++ b/src/GuiApp/FileTime.Avalonia/Converters/ItemViewModeToBrushConverter.cs @@ -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() }; } diff --git a/src/GuiApp/FileTime.Avalonia/IconProviders/IIconProvider.cs b/src/GuiApp/FileTime.Avalonia/IconProviders/IIconProvider.cs new file mode 100644 index 0000000..d24c70d --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/IconProviders/IIconProvider.cs @@ -0,0 +1,9 @@ +using FileTime.Core.Models; + +namespace FileTime.Avalonia.IconProviders +{ + public interface IIconProvider + { + string GetImage(IItem item); + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs b/src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs new file mode 100644 index 0000000..c42e90a --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs @@ -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; + } + } +} diff --git a/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs b/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs index d800dab..33f2669 100644 --- a/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs +++ b/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs @@ -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 ?? ""; } } } diff --git a/src/GuiApp/FileTime.Avalonia/Services/ItemNameConverterService.cs b/src/GuiApp/FileTime.Avalonia/Services/ItemNameConverterService.cs index f75135b..83c0f6c 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/ItemNameConverterService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/ItemNameConverterService.cs @@ -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); diff --git a/src/GuiApp/FileTime.Avalonia/Startup.cs b/src/GuiApp/FileTime.Avalonia/Startup.cs index 6df59fd..d44fc52 100644 --- a/src/GuiApp/FileTime.Avalonia/Startup.cs +++ b/src/GuiApp/FileTime.Avalonia/Startup.cs @@ -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; } } diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs index cbb3ebf..a0c554f 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs @@ -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 _containers = new ObservableCollection(); - - //[Property] - private readonly ObservableCollection _elements = new ObservableCollection(); - - //[Property] - private readonly ObservableCollection _items = new ObservableCollection(); - [Property] private bool _isAlternative; + [Property] + private bool _isMarked; + + [Property] + private ContainerViewModel? _parent; + + public IItem Item => _container; + + private readonly ObservableCollection _containers = new ObservableCollection(); + + private readonly ObservableCollection _elements = new ObservableCollection(); + + private readonly ObservableCollection _items = new ObservableCollection(); + + public List ChildrenToAdopt { get; } = new List(); + [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 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 item, Func 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> GetContainers() { - if (!isInitialized) await Task.Run(Refresh); + if (!_isInitialized) await Task.Run(Refresh); return _containers; } public async Task> GetElements() { - if (!isInitialized) await Task.Run(Refresh); + if (!_isInitialized) await Task.Run(Refresh); return _elements; } public async Task> GetItems() { - if (!isInitialized) await Task.Run(Refresh); + if (!_isInitialized) await Task.Run(Refresh); return _items; } } diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ElementViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ElementViewModel.cs index 594a3f1..cb2723e 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ElementViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ElementViewModel.cs @@ -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 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)); diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/IItemViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/IItemViewModel.cs index 8feee88..6cdd42a 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/IItemViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/IItemViewModel.cs @@ -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; } diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemViewMode.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemViewMode.cs index f61147a..3bae975 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemViewMode.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemViewMode.cs @@ -6,8 +6,11 @@ namespace FileTime.Avalonia.ViewModels { public enum ItemViewMode { - Selected, + Default, Alternative, - Default + Selected, + Marked, + MarkedSelected, + MarkedAlternative } } diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs index 362ddc8..d270b26 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs @@ -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 _commandBindings = new(); private List _universalCommandBindings = new(); + private IClipboard _clipboard; + private TimeRunner _timeRunner; + private Action? _inputHandler; [Property] @@ -51,10 +58,16 @@ namespace FileTime.Avalonia.ViewModels [Property] private List _rootDriveInfos; - public Action? FocusDefaultElement { get; set; } + [Property] + private string _messageBoxText; + + public IReadOnlyList TimelineCommands => _timeRunner.ParallelCommands; async partial void OnInitialize() { + _clipboard = App.ServiceProvider.GetService()!; + _timeRunner = App.ServiceProvider.GetService()!; + _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() { 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(); + + 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(); + + return Task.CompletedTask; + } + + public async Task Delete() + { + IList? 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().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() + { + 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() { 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 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 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() { diff --git a/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml b/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml index b2f6855..eb16276 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml +++ b/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml @@ -17,7 +17,7 @@ Height="18" HorizontalAlignment="Left" VerticalAlignment="Center" - Source="{SvgImage /Assets/material/folder.svg}" /> + Source="{Binding Converter={StaticResource ItemToImageConverter}}" /> - - + + + + + + + + + + + + + + + + + + + + + + + - + @@ -123,7 +144,7 @@ - + @@ -132,7 +153,7 @@ @@ -146,8 +167,8 @@ + IsEnabled="False" + Items="{Binding AppState.SelectedTab.Parent.Items}"> @@ -202,6 +223,7 @@ @@ -223,9 +245,78 @@ + + + + + + + + + + + + + + + + + + + + + + + +