diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Models/Enums/ViewMode.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Models/Enums/ViewMode.cs new file mode 100644 index 0000000..5cff236 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Models/Enums/ViewMode.cs @@ -0,0 +1,8 @@ +namespace FileTime.App.Core.Models.Enums +{ + public enum ViewMode + { + Default, + RapidTravel + } +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/ICommandHandlerService.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/ICommandHandlerService.cs new file mode 100644 index 0000000..4f43ccc --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/ICommandHandlerService.cs @@ -0,0 +1,9 @@ +using FileTime.App.Core.Command; + +namespace FileTime.App.Core.Services +{ + public interface ICommandHandlerService + { + Task HandleCommandAsync(Commands command); + } +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/IAppState.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs similarity index 66% rename from src/AppCommon/FileTime.App.Core.Abstraction/IAppState.cs rename to src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs index b764327..1b322ef 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/IAppState.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs @@ -1,14 +1,14 @@ using System.Collections.ObjectModel; -using FileTime.App.Core.ViewModels; +using FileTime.App.Core.Models.Enums; -namespace FileTime.App.Core +namespace FileTime.App.Core.ViewModels { public interface IAppState { ObservableCollection Tabs { get; } - ITabViewModel? SelectedTab { get; } - IObservable SelectedTabObservable { get; } + IObservable SelectedTab { get; } IObservable SearchText { get; } + ViewMode ViewMode { get; } void AddTab(ITabViewModel tabViewModel); void RemoveTab(ITabViewModel tabViewModel); diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerViewModel.cs index 0f482c5..3669bfe 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerViewModel.cs @@ -5,5 +5,6 @@ namespace FileTime.App.Core.ViewModels { public interface IContainerViewModel : IItemViewModel, IInitable { + IContainer? Container { get; } } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs index 7a5b9db..1d79d65 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs @@ -12,7 +12,7 @@ namespace FileTime.App.Core.ViewModels IObservable IsSelected { get; } IObservable? CurrentLocation { get; } IObservable? CurrentSelectedItem { get; } - IObservable>? CurrentItems { get; } - IObservable> MarkedItems { get; } + IObservable>? CurrentItems { get; } + IObservable> MarkedItems { get; } } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/AppStateBase.cs b/src/AppCommon/FileTime.App.Core/AppStateBase.cs deleted file mode 100644 index e70879d..0000000 --- a/src/AppCommon/FileTime.App.Core/AppStateBase.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Collections.ObjectModel; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using FileTime.App.Core.ViewModels; -using MvvmGen; - -namespace FileTime.App.Core -{ - [ViewModel] - public abstract partial class AppStateBase : IAppState - { - private readonly BehaviorSubject _searchText = new(null); - private readonly BehaviorSubject _selectedTabObservable = new(null); - private ITabViewModel? _selectedTab; - - public ObservableCollection Tabs { get; } = new(); - public IObservable SearchText { get; private set; } - - public IObservable SelectedTabObservable { get; private set; } - public ITabViewModel? SelectedTab - { - get => _selectedTab; - private set - { - if (value != _selectedTab) - { - _selectedTab = value; - OnPropertyChanged(nameof(SelectedTab)); - _selectedTabObservable.OnNext(value); - } - } - } - - partial void OnInitialize() - { - SearchText = _searchText.AsObservable(); - SelectedTabObservable = _selectedTabObservable.AsObservable(); - } - - public void AddTab(ITabViewModel tabViewModel) - { - Tabs.Add(tabViewModel); - if (_selectedTab == null) - { - SelectedTab = Tabs.First(); - } - } - - public void RemoveTab(ITabViewModel tabViewModel) - { - if (!Tabs.Contains(tabViewModel)) return; - - Tabs.Remove(tabViewModel); - if (_selectedTab == tabViewModel) - { - SelectedTab = Tabs.FirstOrDefault(); - } - } - - public void SetSearchText(string? searchText) - { - _searchText.OnNext(searchText); - } - - public void SetSelectedTab(ITabViewModel tabToSelect) - { - SelectedTab = tabToSelect; - } - } -} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj b/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj index bd8eec7..19a40eb 100644 --- a/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj +++ b/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj @@ -7,6 +7,7 @@ + diff --git a/src/AppCommon/FileTime.App.Core/Services/CommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/CommandHandlerService.cs new file mode 100644 index 0000000..faaa5f6 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core/Services/CommandHandlerService.cs @@ -0,0 +1,104 @@ +using System.Reactive.Linq; +using FileTime.App.Core.Command; +using FileTime.App.Core.ViewModels; +using FileTime.Core.Models; + +namespace FileTime.App.Core.Services +{ + public class CommandHandlerService : ICommandHandlerService + { + private readonly Dictionary> _commandHandlers; + private readonly IAppState _appState; + private ITabViewModel? _selectedTab; + private IContainer? _currentLocation; + private IItemViewModel? _currentSelectedItem; + + public CommandHandlerService(IAppState appState) + { + _appState = appState; + + _appState.SelectedTab.Subscribe(t => _selectedTab = t); + _appState.SelectedTab.Select(t => t == null ? Observable.Return(null) : t.CurrentLocation!).Switch().Subscribe(l => _currentLocation = l); + _appState.SelectedTab.Select(t => t == null ? Observable.Return(null) : t.CurrentSelectedItem!).Switch().Subscribe(l => _currentSelectedItem = l); + + _commandHandlers = new Dictionary> + { + //{Commands.AutoRefresh, ToggleAutoRefresh}, + //{Commands.ChangeTimelineMode, ChangeTimelineMode}, + //{Commands.CloseTab, CloseTab}, + //{Commands.Compress, Compress}, + //{Commands.Copy, Copy}, + //{Commands.CopyHash, CopyHash}, + //{Commands.CopyPath, CopyPath}, + //{Commands.CreateContainer, CreateContainer}, + //{Commands.CreateElement, CreateElement}, + //{Commands.Cut, Cut}, + //{Commands.Edit, Edit}, + //{Commands.EnterRapidTravel, EnterRapidTravelMode}, + //{Commands.FindByName, FindByName}, + //{Commands.FindByNameRegex, FindByNameRegex}, + //{Commands.GoToHome, GotToHome}, + //{Commands.GoToPath, GoToContainer}, + //{Commands.GoToProvider, GotToProvider}, + //{Commands.GoToRoot, GotToRoot}, + {Commands.GoUp, GoUp}, + //{Commands.HardDelete, HardDelete}, + //{Commands.Mark, MarkCurrentItem}, + //{Commands.MoveCursorDown, MoveCursorDown}, + //{Commands.MoveCursorDownPage, MoveCursorDownPage}, + //{Commands.MoveCursorUp, MoveCursorUp}, + //{Commands.MoveCursorUpPage, MoveCursorUpPage}, + //{Commands.MoveToFirst, MoveToFirst}, + //{Commands.MoveToLast, MoveToLast}, + //{Commands.NextTimelineBlock, SelectNextTimelineBlock}, + //{Commands.NextTimelineCommand, SelectNextTimelineCommand}, + {Commands.Open, OpenContainer}, + //{Commands.OpenInFileBrowser, OpenInDefaultFileExplorer}, + //{Commands.OpenOrRun, OpenOrRun}, + //{Commands.PasteMerge, PasteMerge}, + //{Commands.PasteOverwrite, PasteOverwrite}, + //{Commands.PasteSkip, PasteSkip}, + //{Commands.PinFavorite, PinFavorite}, + //{Commands.PreviousTimelineBlock, SelectPreviousTimelineBlock}, + //{Commands.PreviousTimelineCommand, SelectPreviousTimelineCommand}, + //{Commands.Refresh, RefreshCurrentLocation}, + //{Commands.Rename, Rename}, + //{Commands.RunCommand, RunCommandInContainer}, + //{Commands.ScanContainerSize, ScanContainerSize}, + //{Commands.ShowAllShotcut, ShowAllShortcut}, + //{Commands.SoftDelete, SoftDelete}, + //{Commands.SwitchToLastTab, async() => await SwitchToTab(-1)}, + //{Commands.SwitchToTab1, async() => await SwitchToTab(1)}, + //{Commands.SwitchToTab2, async() => await SwitchToTab(2)}, + //{Commands.SwitchToTab3, async() => await SwitchToTab(3)}, + //{Commands.SwitchToTab4, async() => await SwitchToTab(4)}, + //{Commands.SwitchToTab5, async() => await SwitchToTab(5)}, + //{Commands.SwitchToTab6, async() => await SwitchToTab(6)}, + //{Commands.SwitchToTab7, async() => await SwitchToTab(7)}, + //{Commands.SwitchToTab8, async() => await SwitchToTab(8)}, + //{Commands.TimelinePause, PauseTimeline}, + //{Commands.TimelineRefresh, RefreshTimeline}, + //{Commands.TimelineStart, ContinueTimeline}, + //{Commands.ToggleAdvancedIcons, ToggleAdvancedIcons}, + //{Commands.ToggleHidden, ToggleHidden}, + }; + } + + public async Task HandleCommandAsync(Commands command) => + await _commandHandlers[command].Invoke(); + + private Task OpenContainer() + { + if (_currentSelectedItem is not IContainerViewModel containerViewModel || containerViewModel.Container is null) return Task.CompletedTask; + + _selectedTab?.Tab?.SetCurrentLocation(containerViewModel.Container); + return Task.CompletedTask; + } + + private async Task GoUp() + { + if (_currentLocation?.Parent is not IAbsolutePath parentPath || await parentPath.ResolveAsync() is not IContainer newContainer) return; + _selectedTab?.Tab?.SetCurrentLocation(newContainer); + } + } +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs new file mode 100644 index 0000000..34ab930 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs @@ -0,0 +1,55 @@ +using System.Collections.ObjectModel; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using FileTime.App.Core.Models.Enums; +using FileTime.App.Core.ViewModels; +using MvvmGen; +using MoreLinq; + +namespace FileTime.App.Core.ViewModels +{ + [ViewModel] + public abstract partial class AppStateBase : IAppState + { + private readonly BehaviorSubject _searchText = new(null); + private readonly BehaviorSubject _selectedTab = new(null); + private readonly BehaviorSubject> _tabs = new(Enumerable.Empty()); + + [Property] + private ViewMode _viewMode; + + public ObservableCollection Tabs { get; } = new(); + public IObservable SearchText { get; private set; } + + public IObservable SelectedTab { get; private set; } + + partial void OnInitialize() + { + Tabs.CollectionChanged += (_, _) => _tabs.OnNext(Tabs); + SearchText = _searchText.AsObservable(); + SelectedTab = Observable.CombineLatest(_tabs, _selectedTab, GetSelectedTab); + } + + public void AddTab(ITabViewModel tabViewModel) + { + Tabs.Add(tabViewModel); + } + + public void RemoveTab(ITabViewModel tabViewModel) + { + if (!Tabs.Contains(tabViewModel)) return; + + Tabs.Remove(tabViewModel); + } + + public void SetSearchText(string? searchText) => _searchText.OnNext(searchText); + + public void SetSelectedTab(ITabViewModel tabToSelect) => _selectedTab.OnNext(tabToSelect); + + private ITabViewModel? GetSelectedTab(IEnumerable tabs, ITabViewModel? expectedSelectedTab) + { + var (prefered, others) = tabs.OrderBy(t => t.TabNumber).Partition(t => t.TabNumber >= (expectedSelectedTab?.TabNumber ?? 0)); + return prefered.Concat(others).FirstOrDefault(); + } + } +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs index 105e6cd..bd492a7 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs @@ -7,6 +7,8 @@ namespace FileTime.App.Core.ViewModels [ViewModel(GenerateConstructor = false)] public partial class ContainerViewModel : ItemViewModel, IContainerViewModel { + public IContainer? Container => BaseItem as IContainer; + public ContainerViewModel(IItemNameConverterService _itemNameConverterService, IAppState _appState) : base(_itemNameConverterService, _appState) { } diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs index 389c00c..21dedd1 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs @@ -24,8 +24,8 @@ namespace FileTime.App.Core.ViewModels public IObservable? CurrentLocation { get; private set; } public IObservable? CurrentSelectedItem { get; private set; } - public IObservable>? CurrentItems { get; private set; } - public IObservable> MarkedItems { get; } + public IObservable>? CurrentItems { get; private set; } + public IObservable> MarkedItems { get; } public TabViewModel( IServiceProvider serviceProvider, @@ -37,7 +37,7 @@ namespace FileTime.App.Core.ViewModels _appState = appState; MarkedItems = _markedItems.Select(e => e.ToList()).AsObservable(); - IsSelected = _appState.SelectedTabObservable.Select(s => s == this); + IsSelected = _appState.SelectedTab.Select(s => s == this); } public void Init(ITab tab, int tabNumber) @@ -46,11 +46,15 @@ namespace FileTime.App.Core.ViewModels TabNumber = tabNumber; CurrentLocation = tab.CurrentLocation.AsObservable(); - CurrentItems = tab.CurrentItems.Select(items => items.Select(MapItemToViewModel).ToList()); - CurrentSelectedItem = Observable.CombineLatest( - CurrentItems, - tab.CurrentSelectedItem, - (currentItems, currentSelectedItemPath) => currentItems.FirstOrDefault(i => i.BaseItem?.FullName == currentSelectedItemPath?.Path)); + CurrentItems = tab.CurrentItems.Select(items => items.Select(MapItemToViewModel).ToList()).Publish(Enumerable.Empty()).RefCount(); + CurrentSelectedItem = + Observable.CombineLatest( + CurrentItems, + tab.CurrentSelectedItem, + (currentItems, currentSelectedItemPath) => currentItems.FirstOrDefault(i => i.BaseItem?.FullName == currentSelectedItemPath?.Path) + ) + .Publish(null) + .RefCount(); tab.CurrentLocation.Subscribe((_) => _markedItems.OnNext(Enumerable.Empty())); } diff --git a/src/ConsoleApp/FileTime.ConsoleUI/Program.cs b/src/ConsoleApp/FileTime.ConsoleUI/Program.cs index 3751555..17b3ef8 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/Program.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI/Program.cs @@ -1,2 +1,7 @@ // See https://aka.ms/new-console-template for more information Console.WriteLine("Hello, World!"); + +var d1 = new DirectoryInfo("C:"); +var d2 = new DirectoryInfo("C:\\"); + +; \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Models/Constants.cs b/src/Core/FileTime.Core.Abstraction/Models/Constants.cs index c414f03..7244ea6 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/Constants.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/Constants.cs @@ -3,6 +3,5 @@ namespace FileTime.Core.Models public static class Constants { public const char SeparatorChar = '/'; - public const int MaximumObservableMergeOperations = 4; } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Models/FullName.cs b/src/Core/FileTime.Core.Abstraction/Models/FullName.cs index 84840dd..406f5dd 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/FullName.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/FullName.cs @@ -6,7 +6,7 @@ namespace FileTime.Core.Models { if (Path is null) return null; - var pathParts = Path.Split(Constants.SeparatorChar); + var pathParts = Path.TrimEnd(Constants.SeparatorChar).Split(Constants.SeparatorChar); return pathParts.Length switch { > 1 => new(string.Join(Constants.SeparatorChar, pathParts.SkipLast(1))), diff --git a/src/Core/FileTime.Core.Abstraction/Models/IAbsolutePath.cs b/src/Core/FileTime.Core.Abstraction/Models/IAbsolutePath.cs index 7cc5837..bfc45dc 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/IAbsolutePath.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/IAbsolutePath.cs @@ -9,5 +9,7 @@ namespace FileTime.Core.Models IContentProvider? VirtualContentProvider { get; } FullName Path { get; } AbsolutePathType Type { get; } + + Task ResolveAsync(); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Models/IItem.cs b/src/Core/FileTime.Core.Abstraction/Models/IItem.cs index d224e01..d88a593 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/IItem.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/IItem.cs @@ -9,7 +9,7 @@ namespace FileTime.Core.Models string DisplayName { get; } FullName? FullName { get; } NativePath? NativePath { get; } - FullName? Parent { get; } + IAbsolutePath? Parent { get; } bool IsHidden { get; } bool IsExists { get; } DateTime? CreatedAt { get; } diff --git a/src/Core/FileTime.Core.Abstraction/Models/ItemsTransformator.cs b/src/Core/FileTime.Core.Abstraction/Models/ItemsTransformator.cs new file mode 100644 index 0000000..c16b576 --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/Models/ItemsTransformator.cs @@ -0,0 +1,7 @@ +namespace FileTime.Core.Models +{ + public record ItemsTransformator( + string Name, + Func, Task>> Transformator + ); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Services/ITab.cs b/src/Core/FileTime.Core.Abstraction/Services/ITab.cs index 9c2949e..a99a3ed 100644 --- a/src/Core/FileTime.Core.Abstraction/Services/ITab.cs +++ b/src/Core/FileTime.Core.Abstraction/Services/ITab.cs @@ -1,5 +1,6 @@ using FileTime.Core.Models; using InitableService; +using System.Reactive.Subjects; namespace FileTime.Core.Services { @@ -9,6 +10,10 @@ namespace FileTime.Core.Services IObservable CurrentSelectedItem { get; } IObservable> CurrentItems { get; } - void ChangeLocation(IContainer newLocation); + void SetCurrentLocation(IContainer newLocation); + void AddSelectedItemsTransformator(ItemsTransformator transformator); + void RemoveSelectedItemsTransformator(ItemsTransformator transformator); + void RemoveSelectedItemsTransformatorByName(string name); + void SetSelectedItem(IAbsolutePath newSelectedItem); } } \ 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 f5f41f0..e573edb 100644 --- a/src/Core/FileTime.Core.Models/AbsolutePath.cs +++ b/src/Core/FileTime.Core.Models/AbsolutePath.cs @@ -18,5 +18,11 @@ namespace FileTime.Core.Models VirtualContentProvider = virtualContentProvider; Type = type; } + + public async Task ResolveAsync() + { + var provider = VirtualContentProvider ?? ContentProvider; + return await provider.GetItemByFullNameAsync(Path); + } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Models/Container.cs b/src/Core/FileTime.Core.Models/Container.cs index 2c9cc7b..a9fb656 100644 --- a/src/Core/FileTime.Core.Models/Container.cs +++ b/src/Core/FileTime.Core.Models/Container.cs @@ -10,7 +10,7 @@ namespace FileTime.Core.Models string DisplayName, FullName FullName, NativePath NativePath, - FullName Parent, + IAbsolutePath? Parent, bool IsHidden, bool IsExists, DateTime? CreatedAt, diff --git a/src/Core/FileTime.Core.Models/Element.cs b/src/Core/FileTime.Core.Models/Element.cs index d74adc5..e5018a8 100644 --- a/src/Core/FileTime.Core.Models/Element.cs +++ b/src/Core/FileTime.Core.Models/Element.cs @@ -8,7 +8,7 @@ namespace FileTime.Core.Models string DisplayName, FullName FullName, NativePath NativePath, - FullName Parent, + IAbsolutePath? Parent, bool IsHidden, bool IsExists, DateTime? CreatedAt, diff --git a/src/Core/FileTime.Core.Models/FileElement.cs b/src/Core/FileTime.Core.Models/FileElement.cs index 7de3d8c..3a84236 100644 --- a/src/Core/FileTime.Core.Models/FileElement.cs +++ b/src/Core/FileTime.Core.Models/FileElement.cs @@ -8,7 +8,7 @@ namespace FileTime.Core.Models string DisplayName, FullName FullName, NativePath NativePath, - FullName Parent, + IAbsolutePath? Parent, bool IsHidden, bool IsExists, DateTime? CreatedAt, diff --git a/src/Core/FileTime.Core.Services/ContentProviderBase.cs b/src/Core/FileTime.Core.Services/ContentProviderBase.cs index 5a658fc..d05e576 100644 --- a/src/Core/FileTime.Core.Services/ContentProviderBase.cs +++ b/src/Core/FileTime.Core.Services/ContentProviderBase.cs @@ -29,7 +29,7 @@ namespace FileTime.Core.Services public IContentProvider Provider => this; - public FullName? Parent => null; + public IAbsolutePath? Parent => null; public DateTime? CreatedAt => null; diff --git a/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj b/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj index ac049af..9615311 100644 --- a/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj +++ b/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Core/FileTime.Core.Services/Tab.cs b/src/Core/FileTime.Core.Services/Tab.cs index 280e13b..f33575e 100644 --- a/src/Core/FileTime.Core.Services/Tab.cs +++ b/src/Core/FileTime.Core.Services/Tab.cs @@ -8,45 +8,71 @@ namespace FileTime.Core.Services { private readonly BehaviorSubject _currentLocation = new(null); private readonly BehaviorSubject _currentSelectedItem = new(null); + private readonly List _transformators = new(); + private IAbsolutePath? _currentSelectedItemCached; public IObservable CurrentLocation { get; } public IObservable> CurrentItems { get; } public IObservable CurrentSelectedItem { get; } public Tab() { - CurrentLocation = _currentLocation.AsObservable(); - CurrentItems = + CurrentLocation = _currentLocation.DistinctUntilChanged().Do(_ => {; }).Publish(null).RefCount(); + CurrentItems = Observable.Merge( - _currentLocation + CurrentLocation .Where(c => c is not null) .Select(c => c!.Items) .Switch() - .Select( - i => Observable.FromAsync(async () => - await i - .ToAsyncEnumerable() - .SelectAwait( - async i => - { - try - { - //TODO: force create by AbsolutePath name - return await i.ContentProvider.GetItemByFullNameAsync(i.Path); - } - catch { return null!; } - } - ) - .Where(i => i != null) - .ToListAsync() - ) - ) - .Merge(Constants.MaximumObservableMergeOperations), - _currentLocation + .Select(i => Observable.FromAsync(async () => await MapItems(i))) + .Switch(), + CurrentLocation .Where(c => c is null) - .Select(c => Enumerable.Empty()) - ); + .Select(_ => Enumerable.Empty()) + ) + .Publish(Enumerable.Empty()) + .RefCount(); - CurrentSelectedItem = CurrentLocation.Select(GetSelectedItemByLocation).Switch().Merge(_currentSelectedItem).Throttle(TimeSpan.FromMilliseconds(500)); + CurrentSelectedItem = CurrentLocation + .Select(GetSelectedItemByLocation) + .Switch() + .Merge(_currentSelectedItem) + .DistinctUntilChanged() + .Publish(null) + .RefCount(); + + CurrentSelectedItem.Subscribe(s => + { + _currentSelectedItemCached = s; + _currentSelectedItem.OnNext(s); + }); + } + + private async Task> MapItems(IReadOnlyList items) + { + IEnumerable resolvedItems = await items + .ToAsyncEnumerable() + .SelectAwait( + async i => + { + try + { + //TODO: force create by AbsolutePath name + return await i.ContentProvider.GetItemByFullNameAsync(i.Path); + } + catch { return null!; } + } + ) + .Where(i => i != null) + .ToListAsync(); + + return _transformators.Count == 0 + ? resolvedItems + : (await _transformators + .ToAsyncEnumerable() + .Scan(resolvedItems, (acc, t) => new ValueTask>(t.Transformator(acc))) + .ToListAsync() + ) + .SelectMany(t => t); } public void Init(IContainer currentLocation) @@ -56,12 +82,25 @@ namespace FileTime.Core.Services private IObservable GetSelectedItemByLocation(IContainer? currentLocation) { + //TODO: return currentLocation?.Items?.Select(i => i.FirstOrDefault()) ?? Observable.Never((IAbsolutePath?)null); } - public void ChangeLocation(IContainer newLocation) + public void SetCurrentLocation(IContainer newLocation) => _currentLocation.OnNext(newLocation); + + public void SetSelectedItem(IAbsolutePath newSelectedItem) => _currentSelectedItem.OnNext(newSelectedItem); + + public void AddSelectedItemsTransformator(ItemsTransformator transformator) => _transformators.Add(transformator); + public void RemoveSelectedItemsTransformator(ItemsTransformator transformator) => _transformators.Remove(transformator); + public void RemoveSelectedItemsTransformatorByName(string name) => _transformators.RemoveAll(t => t.Name == name); + + public async Task OpenSelected() { - _currentLocation.OnNext(newLocation); + if (_currentSelectedItemCached == null) return; + var resolvedSelectedItem = await _currentSelectedItemCached.ContentProvider.GetItemByFullNameAsync(_currentSelectedItemCached.Path); + + if (resolvedSelectedItem is not IContainer resolvedContainer) return; + SetCurrentLocation(resolvedContainer); } } } \ No newline at end of file diff --git a/src/FileTime.sln b/src/FileTime.sln index a86ec00..36e1f00 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -45,6 +45,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.GuiApp.CustomImpl" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.Providers.Local.Abstractions", "Providers\FileTime.Providers.Local.Abstractions\FileTime.Providers.Local.Abstractions.csproj", "{1500A537-2116-4111-B216-7632040619B0}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.GuiApp.Abstractions", "GuiApp\Avalonia\FileTime.GuiApp.Abstractions\FileTime.GuiApp.Abstractions.csproj", "{D7D1C76A-05B0-49BC-BCFF-06340E264EC1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -107,6 +109,10 @@ Global {1500A537-2116-4111-B216-7632040619B0}.Debug|Any CPU.Build.0 = Debug|Any CPU {1500A537-2116-4111-B216-7632040619B0}.Release|Any CPU.ActiveCfg = Release|Any CPU {1500A537-2116-4111-B216-7632040619B0}.Release|Any CPU.Build.0 = Release|Any CPU + {D7D1C76A-05B0-49BC-BCFF-06340E264EC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7D1C76A-05B0-49BC-BCFF-06340E264EC1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7D1C76A-05B0-49BC-BCFF-06340E264EC1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7D1C76A-05B0-49BC-BCFF-06340E264EC1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -127,6 +133,7 @@ Global {26331AB9-6E4D-40DB-8FF0-CB7133F67CA0} = {01F231DE-4A65-435F-B4BB-77EE5221890C} {4B742649-225F-4C73-B118-1B29FE2A5774} = {01F231DE-4A65-435F-B4BB-77EE5221890C} {1500A537-2116-4111-B216-7632040619B0} = {2FC40FE1-4446-44AB-BF77-00F94D995FA3} + {D7D1C76A-05B0-49BC-BCFF-06340E264EC1} = {01F231DE-4A65-435F-B4BB-77EE5221890C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/CommandBindingConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/CommandBindingConfiguration.cs similarity index 97% rename from src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/CommandBindingConfiguration.cs rename to src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/CommandBindingConfiguration.cs index d1b1472..22dcd0e 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/CommandBindingConfiguration.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/CommandBindingConfiguration.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Avalonia.Input; using FileTime.App.Core.Command; diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyBindingConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/KeyBindingConfiguration.cs similarity index 90% rename from src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyBindingConfiguration.cs rename to src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/KeyBindingConfiguration.cs index 4b2eed6..7fec8f1 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyBindingConfiguration.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/KeyBindingConfiguration.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace FileTime.GuiApp.Configuration { public class KeyBindingConfiguration diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyConfig.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/KeyConfig.cs similarity index 52% rename from src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyConfig.cs rename to src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/KeyConfig.cs index 8701239..902319b 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyConfig.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/KeyConfig.cs @@ -11,12 +11,22 @@ namespace FileTime.GuiApp.Configuration public KeyConfig() { } - public KeyConfig(Key key, bool shift = false, bool alt = false, bool ctrl = false) + public KeyConfig( + Key key, + bool shift = false, + bool alt = false, + bool ctrl = false) { Key = key; Shift = shift; Alt = alt; Ctrl = ctrl; } + + public bool AreEquals(KeyConfig otherKeyConfig) => + Key == otherKeyConfig.Key + && Alt == otherKeyConfig.Alt + && Shift == otherKeyConfig.Shift + && Ctrl == otherKeyConfig.Ctrl; } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/MainConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs similarity index 100% rename from src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/MainConfiguration.cs rename to src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/ProgramConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/ProgramConfiguration.cs similarity index 100% rename from src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/ProgramConfiguration.cs rename to src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/ProgramConfiguration.cs diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/ProgramsConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/ProgramsConfiguration.cs similarity index 100% rename from src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/ProgramsConfiguration.cs rename to src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/ProgramsConfiguration.cs diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/SectionNames.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/SectionNames.cs similarity index 100% rename from src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/SectionNames.cs rename to src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/SectionNames.cs diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/FileTime.GuiApp.Abstractions.csproj b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/FileTime.GuiApp.Abstractions.csproj new file mode 100644 index 0000000..e4d07e5 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/FileTime.GuiApp.Abstractions.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Models/SpecialKeysStatus.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Models/SpecialKeysStatus.cs new file mode 100644 index 0000000..10b54d1 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Models/SpecialKeysStatus.cs @@ -0,0 +1,4 @@ +namespace FileTime.GuiApp.Models +{ + public record SpecialKeysStatus(bool IsAltPressed, bool IsShiftPressed, bool IsCtrlPressed); +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IDefaultModeKeyInputHandler.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IDefaultModeKeyInputHandler.cs new file mode 100644 index 0000000..9d84b42 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IDefaultModeKeyInputHandler.cs @@ -0,0 +1,4 @@ +namespace FileTime.GuiApp.Services +{ + public interface IDefaultModeKeyInputHandler : IKeyInputHandler { } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IKeyInputHandler.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IKeyInputHandler.cs new file mode 100644 index 0000000..1ae5f77 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IKeyInputHandler.cs @@ -0,0 +1,10 @@ +using Avalonia.Input; +using FileTime.GuiApp.Models; + +namespace FileTime.GuiApp.Services +{ + public interface IKeyInputHandler + { + Task HandleInputKey(Key key, SpecialKeysStatus specialKeysStatus, Action setHandled); + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IKeyInputHandlerService.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IKeyInputHandlerService.cs new file mode 100644 index 0000000..7da92c9 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IKeyInputHandlerService.cs @@ -0,0 +1,9 @@ +using Avalonia.Input; + +namespace FileTime.GuiApp.Services +{ + public interface IKeyInputHandlerService + { + Task ProcessKeyDown(Key key, KeyModifiers keyModifiers, Action setHandled); + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IKeyboardConfigurationService.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IKeyboardConfigurationService.cs new file mode 100644 index 0000000..84f9922 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IKeyboardConfigurationService.cs @@ -0,0 +1,11 @@ +using FileTime.GuiApp.Configuration; + +namespace FileTime.GuiApp.Services +{ + public interface IKeyboardConfigurationService + { + IReadOnlyList CommandBindings { get; } + IReadOnlyList UniversalCommandBindings { get; } + IReadOnlyList AllShortcut { get; } + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IRapidTravelModeKeyInputHandler.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IRapidTravelModeKeyInputHandler.cs new file mode 100644 index 0000000..e15ca90 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IRapidTravelModeKeyInputHandler.cs @@ -0,0 +1,4 @@ +namespace FileTime.GuiApp.Services +{ + public interface IRapidTravelModeKeyInputHandler : IKeyInputHandler { } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/ViewModels/IGuiAppState.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/ViewModels/IGuiAppState.cs new file mode 100644 index 0000000..bf714ec --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/ViewModels/IGuiAppState.cs @@ -0,0 +1,14 @@ +using FileTime.App.Core.ViewModels; +using FileTime.GuiApp.Configuration; + +namespace FileTime.GuiApp.ViewModels +{ + public interface IGuiAppState : IAppState + { + List PreviousKeys { get; } + bool IsAllShortcutVisible { get; set; } + bool NoCommandFound { get; set; } + string? MessageBoxText { get; set; } + List PossibleCommands { get; set; } + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml.cs index 3782f81..d988d57 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml.cs @@ -22,7 +22,7 @@ namespace FileTime.GuiApp .InitSerilog(); var logger = DI.ServiceProvider.GetRequiredService>(); - logger.LogInformation("App initialization completed."); + logger.LogInformation("App initialization completed"); } public override void Initialize() { diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs index 6ae33b2..deae7c3 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs @@ -6,6 +6,7 @@ using FileTime.App.Core.ViewModels; using FileTime.Core.Services; using FileTime.GuiApp.Configuration; using FileTime.GuiApp.Logging; +using FileTime.GuiApp.Services; using FileTime.GuiApp.ViewModels; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -20,14 +21,22 @@ namespace FileTime.GuiApp { return serviceCollection .AddSingleton() - .AddSingleton() - .AddSingleton(s => s.GetRequiredService()) + .AddSingleton() + .AddSingleton(s => s.GetRequiredService()) + .AddSingleton(s => s.GetRequiredService()) + .AddSingleton() + .AddSingleton() //TODO: move?? .AddTransient() .AddTransient() .AddTransient() .AddTransient() - .AddTransient(); + .AddTransient() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); } internal static IServiceCollection RegisterLogging(this IServiceCollection serviceCollection) diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/FileTime.GuiApp.CustomImpl.csproj b/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/FileTime.GuiApp.CustomImpl.csproj index 4f17a4f..92c9dea 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/FileTime.GuiApp.CustomImpl.csproj +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/FileTime.GuiApp.CustomImpl.csproj @@ -8,6 +8,7 @@ + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/ViewModels/AppState.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/ViewModels/AppState.cs deleted file mode 100644 index 8295111..0000000 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/ViewModels/AppState.cs +++ /dev/null @@ -1,6 +0,0 @@ -using FileTime.App.Core; - -namespace FileTime.GuiApp.ViewModels -{ - public class AppState : AppStateBase { } -} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/ViewModels/GuiAppState.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/ViewModels/GuiAppState.cs new file mode 100644 index 0000000..0183ec0 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/ViewModels/GuiAppState.cs @@ -0,0 +1,24 @@ +using FileTime.App.Core.ViewModels; +using FileTime.GuiApp.Configuration; +using MvvmGen; + +namespace FileTime.GuiApp.ViewModels +{ + [ViewModel] + public partial class GuiAppState : AppStateBase, IGuiAppState + { + [Property] + private bool _isAllShortcutVisible; + + [Property] + private bool _noCommandFound; + + [Property] + private string? _messageBoxText; + + [Property] + private List _possibleCommands = new(); + + public List PreviousKeys { get; } = new(); + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Extensions/KeyConfigExtensions.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Extensions/KeyConfigExtensions.cs new file mode 100644 index 0000000..0760077 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Extensions/KeyConfigExtensions.cs @@ -0,0 +1,10 @@ +using FileTime.GuiApp.Configuration; + +namespace FileTime.GuiApp.Extensions +{ + public static class KeyConfigExtensions + { + public static bool AreKeysEqual(this IReadOnlyList collection1, IReadOnlyList collection2) + => collection1.Count == collection2.Count && collection1.Zip(collection2).All(t => t.First.AreEquals(t.Second)); + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj b/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj index 251b33b..7a5eee8 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj @@ -33,6 +33,7 @@ + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/DefaultModeKeyInputHandler.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/DefaultModeKeyInputHandler.cs new file mode 100644 index 0000000..1ebc34b --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/DefaultModeKeyInputHandler.cs @@ -0,0 +1,151 @@ +using System.Reactive.Linq; +using Avalonia.Input; +using FileTime.App.Core.Command; +using FileTime.App.Core.Services; +using FileTime.App.Core.ViewModels; +using FileTime.Core.Models; +using FileTime.GuiApp.Configuration; +using FileTime.GuiApp.Extensions; +using FileTime.GuiApp.Models; +using FileTime.GuiApp.ViewModels; +using Microsoft.Extensions.Logging; + +namespace FileTime.GuiApp.Services +{ + public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler + { + private readonly IGuiAppState _appState; + private readonly IKeyboardConfigurationService _keyboardConfigurationService; + private readonly List _keysToSkip = new(); + private ITabViewModel? _selectedTab; + private IContainer? _currentLocation; + private readonly ILogger _logger; + private readonly ICommandHandlerService _commandHandlerService; + + public DefaultModeKeyInputHandler( + IGuiAppState appState, + IKeyboardConfigurationService keyboardConfigurationService, + ILogger logger, + ICommandHandlerService commandHandlerService) + { + _appState = appState; + _keyboardConfigurationService = keyboardConfigurationService; + _logger = logger; + _commandHandlerService = commandHandlerService; + + _appState.SelectedTab.Subscribe(t => _selectedTab = t); + _appState.SelectedTab.Select(t => t == null ? Observable.Return(null) : t.CurrentLocation!).Switch().Subscribe(l => _currentLocation = l); + + _keysToSkip.Add(new KeyConfig[] { new KeyConfig(Key.Up) }); + _keysToSkip.Add(new KeyConfig[] { new KeyConfig(Key.Down) }); + _keysToSkip.Add(new KeyConfig[] { new KeyConfig(Key.Tab) }); + _keysToSkip.Add(new KeyConfig[] { new KeyConfig(Key.PageDown) }); + _keysToSkip.Add(new KeyConfig[] { new KeyConfig(Key.PageUp) }); + _keysToSkip.Add(new KeyConfig[] { new KeyConfig(Key.F4, alt: true) }); + _keysToSkip.Add(new KeyConfig[] { new KeyConfig(Key.LWin) }); + _keysToSkip.Add(new KeyConfig[] { new KeyConfig(Key.RWin) }); + } + + public async Task HandleInputKey(Key key, SpecialKeysStatus specialKeysStatus, Action setHandled) + { + var keyWithModifiers = new KeyConfig(key, shift: specialKeysStatus.IsShiftPressed, alt: specialKeysStatus.IsAltPressed, ctrl: specialKeysStatus.IsCtrlPressed); + _appState.PreviousKeys.Add(keyWithModifiers); + + var selectedCommandBinding = _keyboardConfigurationService.UniversalCommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(_appState.PreviousKeys)); + selectedCommandBinding ??= _keyboardConfigurationService.CommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(_appState.PreviousKeys)); + + if (key == Key.Escape) + { + var doGeneralReset = false; + if (_appState.PreviousKeys.Count > 1 || _appState.IsAllShortcutVisible || _appState.MessageBoxText != null) + { + doGeneralReset = true; + } + /*else if (_currentLocation.Container.CanHandleEscape) + { + var escapeResult = await _currentLocation.Container.HandleEscape(); + if (escapeResult.NavigateTo != null) + { + setHandled(true); + _appState.PreviousKeys.Clear(); + await _appState.SelectedTab.OpenContainer(escapeResult.NavigateTo); + } + else + { + if (escapeResult.Handled) + { + _appState.PreviousKeys.Clear(); + } + else + { + doGeneralReset = true; + } + } + }*/ + + if (doGeneralReset) + { + setHandled(true); + _appState.IsAllShortcutVisible = false; + _appState.MessageBoxText = null; + _appState.PreviousKeys.Clear(); + _appState.PossibleCommands = new(); + } + } + else if (key == Key.Enter + && _appState.MessageBoxText != null) + { + _appState.PreviousKeys.Clear(); + //_dialogService.ProcessMessageBox(); + setHandled(true); + } + else if (selectedCommandBinding != null) + { + setHandled(true); + _appState.PreviousKeys.Clear(); + _appState.PossibleCommands = new(); + await CallCommandAsync(selectedCommandBinding.Command); + } + else if (_keysToSkip.Any(k => k.AreKeysEqual(_appState.PreviousKeys))) + { + _appState.PreviousKeys.Clear(); + _appState.PossibleCommands = new(); + return; + } + else if (_appState.PreviousKeys.Count == 2) + { + setHandled(true); + _appState.NoCommandFound = true; + _appState.PreviousKeys.Clear(); + _appState.PossibleCommands = new(); + } + else + { + setHandled(true); + var possibleCommands = _keyboardConfigurationService.AllShortcut.Where(c => c.Keys[0].AreEquals(keyWithModifiers)).ToList(); + + if (possibleCommands.Count == 0) + { + _appState.NoCommandFound = true; + _appState.PreviousKeys.Clear(); + } + else + { + _appState.PossibleCommands = possibleCommands; + } + } + } + + private async Task CallCommandAsync(Commands command) + { + try + { + await _commandHandlerService.HandleCommandAsync(command); + } + catch (Exception e) + { + _logger.LogError(e, "Unknown error while running command. {Command} {Error}", command, e); + } + } + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/KeyInputHandlerService.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/KeyInputHandlerService.cs new file mode 100644 index 0000000..ceb187b --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/KeyInputHandlerService.cs @@ -0,0 +1,56 @@ +using Avalonia.Input; +using FileTime.App.Core.Models.Enums; +using FileTime.GuiApp.Configuration; +using FileTime.GuiApp.Models; +using FileTime.GuiApp.ViewModels; + +namespace FileTime.GuiApp.Services +{ + public class KeyInputHandlerService : IKeyInputHandlerService + { + private readonly IGuiAppState _appState; + private readonly IDefaultModeKeyInputHandler _defaultModeKeyInputHandler; + private readonly IRapidTravelModeKeyInputHandler _rapidTravelModeKeyInputHandler; + + public KeyInputHandlerService( + IGuiAppState appState, + IDefaultModeKeyInputHandler defaultModeKeyInputHandler, + IRapidTravelModeKeyInputHandler rapidTravelModeKeyInputHandler + ) + { + _appState = appState; + _defaultModeKeyInputHandler = defaultModeKeyInputHandler; + _rapidTravelModeKeyInputHandler = rapidTravelModeKeyInputHandler; + } + + public async Task ProcessKeyDown(Key key, KeyModifiers keyModifiers, Action setHandled) + { + if (key == Key.LeftAlt + || key == Key.RightAlt + || key == Key.LeftShift + || key == Key.RightShift + || key == Key.LeftCtrl + || key == Key.RightCtrl) + { + return; + } + + //_appState.NoCommandFound = false; + + var isAltPressed = (keyModifiers & KeyModifiers.Alt) == KeyModifiers.Alt; + var isShiftPressed = (keyModifiers & KeyModifiers.Shift) == KeyModifiers.Shift; + var isCtrlPressed = (keyModifiers & KeyModifiers.Control) == KeyModifiers.Control; + + var specialKeyStatus = new SpecialKeysStatus(isAltPressed, isShiftPressed, isCtrlPressed); + + if (_appState.ViewMode == ViewMode.Default) + { + await _defaultModeKeyInputHandler.HandleInputKey(key, specialKeyStatus, setHandled); + } + else + { + await _rapidTravelModeKeyInputHandler.HandleInputKey(key, specialKeyStatus, setHandled); + } + } + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/KeyboardConfigurationService.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/KeyboardConfigurationService.cs new file mode 100644 index 0000000..6429ae4 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/KeyboardConfigurationService.cs @@ -0,0 +1,61 @@ +using FileTime.App.Core.Command; +using FileTime.GuiApp.Configuration; +using Microsoft.Extensions.Options; + +namespace FileTime.GuiApp.Services +{ + public class KeyboardConfigurationService : IKeyboardConfigurationService + { + public IReadOnlyList CommandBindings { get; } + public IReadOnlyList UniversalCommandBindings { get; } + public IReadOnlyList AllShortcut { get; } + + public KeyboardConfigurationService(IOptions keyBindingConfiguration) + { + var commandBindings = new List(); + var universalCommandBindings = new List(); + IEnumerable keyBindings = keyBindingConfiguration.Value.KeyBindings; + + if (keyBindingConfiguration.Value.UseDefaultBindings) + { + keyBindings = keyBindings.Concat(keyBindingConfiguration.Value.DefaultKeyBindings); + } + + foreach (var keyBinding in keyBindings) + { + if (keyBinding.Command == Commands.None) + { + throw new FormatException($"No command is set in keybinding for keys '{keyBinding.KeysDisplayText}'"); + } + else if (keyBinding.Keys.Count == 0) + { + throw new FormatException($"No keys set in keybinding for command '{keyBinding.Command}'."); + } + + if (IsUniversal(keyBinding)) + { + universalCommandBindings.Add(keyBinding); + } + else + { + commandBindings.Add(keyBinding); + } + } + + CommandBindings = commandBindings.AsReadOnly(); + UniversalCommandBindings = universalCommandBindings.AsReadOnly(); + AllShortcut = new List(CommandBindings.Concat(UniversalCommandBindings)).AsReadOnly(); + } + + private static bool IsUniversal(CommandBindingConfiguration keyMapping) + { + return keyMapping.Command == Commands.GoUp + || keyMapping.Command == Commands.Open + || keyMapping.Command == Commands.OpenOrRun + || keyMapping.Command == Commands.MoveCursorUp + || keyMapping.Command == Commands.MoveCursorDown + || keyMapping.Command == Commands.MoveCursorUpPage + || keyMapping.Command == Commands.MoveCursorDownPage; + } + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/RapidTravelModeKeyInputHandler.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/RapidTravelModeKeyInputHandler.cs new file mode 100644 index 0000000..f7a7f05 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/RapidTravelModeKeyInputHandler.cs @@ -0,0 +1,88 @@ +using Avalonia.Input; +using FileTime.GuiApp.Models; + +namespace FileTime.GuiApp.Services +{ + public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler + { + public async Task HandleInputKey(Key key, SpecialKeysStatus specialKeysStatus, Action setHandled) + { + /*var keyString = key.ToString(); + var updateRapidTravelFilter = false; + + if (key == Key.Escape) + { + setHandled(true); + if (_appState.IsAllShortcutVisible) + { + _appState.IsAllShortcutVisible = false; + } + else if (_appState.MessageBoxText != null) + { + _appState.MessageBoxText = null; + } + else + { + await _appState.ExitRapidTravelMode(); + } + } + else if (key == Key.Back) + { + if (_appState.RapidTravelText.Length > 0) + { + setHandled(true); + _appState.RapidTravelText = _appState.RapidTravelText.Substring(0, _appState.RapidTravelText.Length - 1); + updateRapidTravelFilter = true; + } + } + else if (keyString.Length == 1) + { + setHandled(true); + _appState.RapidTravelText += keyString.ToLower(); + updateRapidTravelFilter = true; + } + else + { + var currentKeyAsList = new List() { new KeyConfig(key) }; + var selectedCommandBinding = _keyboardConfigurationService.UniversalCommandBindings.FirstOrDefault(c => AreKeysEqual(c.Keys, currentKeyAsList)); + if (selectedCommandBinding != null) + { + setHandled(true); + await CallCommandAsync(selectedCommandBinding.Command); + } + } + + if (updateRapidTravelFilter) + { + var currentLocation = await _appState.SelectedTab.CurrentLocation.Container.WithoutVirtualContainer(MainPageViewModel.RAPIDTRAVEL); + var newLocation = new VirtualContainer( + currentLocation, + new List, IEnumerable>>() + { + container => container.Where(c => c.Name.ToLower().Contains(_appState.RapidTravelText)) + }, + new List, IEnumerable>>() + { + element => element.Where(e => e.Name.ToLower().Contains(_appState.RapidTravelText)) + }, + virtualContainerName: MainPageViewModel.RAPIDTRAVEL + ); + + await newLocation.Init(); + + await _appState.SelectedTab.OpenContainer(newLocation); + + var selectedItemName = _appState.SelectedTab.SelectedItem?.Item.Name; + var currentLocationItems = await _appState.SelectedTab.CurrentLocation.GetItems(); + if (currentLocationItems.FirstOrDefault(i => string.Equals(i.Item.Name, _appState.RapidTravelText, StringComparison.OrdinalIgnoreCase)) is IItemViewModel matchItem) + { + await _appState.SelectedTab.SetCurrentSelectedItem(matchItem.Item); + } + else if (!currentLocationItems.Select(i => i.Item.Name).Any(n => n == selectedItemName)) + { + await _appState.SelectedTab.MoveCursorToFirst(); + } + }*/ + } + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/MainWindowViewModel.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/MainWindowViewModel.cs index 8151fa4..fb55618 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/MainWindowViewModel.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/MainWindowViewModel.cs @@ -5,6 +5,7 @@ using FileTime.App.Core; using FileTime.App.Core.ViewModels; using FileTime.Core.Models; using FileTime.Core.Services; +using FileTime.GuiApp.Services; using FileTime.Providers.Local; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -17,6 +18,7 @@ namespace FileTime.GuiApp.ViewModels [Inject(typeof(ILocalContentProvider), "_localContentProvider")] [Inject(typeof(IServiceProvider), PropertyName = "_serviceProvider")] [Inject(typeof(ILogger), PropertyName = "_logger")] + [Inject(typeof(IKeyInputHandlerService), PropertyName = "_keyInputHandlerService")] public partial class MainWindowViewModel : IMainWindowViewModelBase { public bool Loading => false; @@ -51,6 +53,7 @@ namespace FileTime.GuiApp.ViewModels public void ProcessKeyDown(Key key, KeyModifiers keyModifiers, Action setHandled) { + _keyInputHandlerService.ProcessKeyDown(key, keyModifiers, setHandled); } } } diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml index 5c858bb..671c2c7 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml @@ -14,14 +14,11 @@ TransparencyLevelHint="Blur" Background="Transparent" ExtendClientAreaToDecorationsHint="True" - x:DataType="vm:MainWindowViewModel" + x:DataType="vm:IMainWindowViewModelBase" x:CompileBindings="True"> - - - - + @@ -32,9 +29,9 @@ - + + Text="{Binding AppState.SelectedTab^.CurrentSelectedItem^.DisplayNameText}" Foreground="{StaticResource AccentBrush}" /> @@ -89,7 +86,7 @@ - + @@ -117,7 +114,7 @@ HorizontalAlignment="Center" FontWeight="Bold" Foreground="{DynamicResource ErrorBrush}" - IsVisible="{Binding AppState.SelectedTab.CurrentLocation^.Items^.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"> + IsVisible="{Binding AppState.SelectedTab^.CurrentLocation^.Items^.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"> Empty diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.DirectoryHelper.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.DirectoryHelper.cs index 9ffc9e4..2155e09 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.DirectoryHelper.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.DirectoryHelper.cs @@ -19,5 +19,17 @@ namespace FileTime.Providers.Local + ((directoryInfo.Attributes & FileAttributes.System) == FileAttributes.System ? "s" : "-"); } } + + private static IEnumerable GetFilesSafe(DirectoryInfo directoryInfo) + { + try + { + return directoryInfo.GetFiles(); + } + catch + { + return Enumerable.Empty(); + } + } } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs index a83f4c3..7465b78 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -34,9 +34,13 @@ namespace FileTime.Providers.Local public override Task GetItemByNativePathAsync(NativePath nativePath) { var path = nativePath.Path; - if (Directory.Exists(path)) + if ((path?.Length ?? 0) == 0) { - return Task.FromResult((IItem)DirectoryToContainer(new DirectoryInfo(path))); + return Task.FromResult((IItem)this); + } + else if (Directory.Exists(path)) + { + return Task.FromResult((IItem)DirectoryToContainer(new DirectoryInfo(path!.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar))); } else if (File.Exists(path)) { @@ -48,7 +52,7 @@ namespace FileTime.Providers.Local public override Task> GetItemsByContainerAsync(FullName fullName) => Task.FromResult(GetItemsByContainer(fullName)); public List GetItemsByContainer(FullName fullName) => GetItemsByContainer(new DirectoryInfo(GetNativePath(fullName).Path)); - public List GetItemsByContainer(DirectoryInfo directoryInfo) => directoryInfo.GetDirectories().Select(DirectoryToAbsolutePath).Concat(directoryInfo.GetFiles().Select(FileToAbsolutePath)).ToList(); + public List GetItemsByContainer(DirectoryInfo directoryInfo) => directoryInfo.GetDirectories().Select(DirectoryToAbsolutePath).Concat(GetFilesSafe(directoryInfo).Select(FileToAbsolutePath)).ToList(); private IAbsolutePath DirectoryToAbsolutePath(DirectoryInfo directoryInfo) { @@ -65,12 +69,18 @@ namespace FileTime.Providers.Local private Container DirectoryToContainer(DirectoryInfo directoryInfo) { var fullName = GetFullName(directoryInfo.FullName); + var parentFullName = fullName.GetParent(); + var parent = new AbsolutePath( + this, + parentFullName ?? new FullName(""), + AbsolutePathType.Container); + return new( directoryInfo.Name, directoryInfo.Name, fullName, new(directoryInfo.FullName), - fullName.GetParent()!, + parent, (directoryInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden, directoryInfo.Exists, directoryInfo.CreationTime, @@ -85,12 +95,15 @@ namespace FileTime.Providers.Local private Element FileToElement(FileInfo fileInfo) { var fullName = GetFullName(fileInfo); + var parentFullName = fullName.GetParent() ?? throw new Exception($"Path does not have parent: '{fileInfo.FullName}'"); + var parent = new AbsolutePath(this, parentFullName, AbsolutePathType.Container); + return new( fileInfo.Name, fileInfo.Name, fullName, new(fileInfo.FullName), - fullName.GetParent()!, + parent, (fileInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden, fileInfo.Exists, fileInfo.CreationTime,