using System.Collections.ObjectModel; using System.Diagnostics; using DeclarativeProperty; using FileTime.App.CommandPalette.Services; using FileTime.App.Core.Configuration; using FileTime.App.Core.Extensions; using FileTime.App.Core.Models.Enums; using FileTime.App.Core.UserCommand; using FileTime.App.Core.ViewModels; using FileTime.App.FrequencyNavigation.Services; using FileTime.Core.ContentAccess; using FileTime.Core.Interactions; using FileTime.Core.Models; using FileTime.Core.Services; using FileTime.Core.Timeline; using FileTime.Providers.Local; using InitableService; using Microsoft.Extensions.Logging; namespace FileTime.App.Core.Services.UserCommandHandler; public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase { private const int PageSize = 8; private readonly IAppState _appState; private readonly IServiceProvider _serviceProvider; private readonly ILocalContentProvider _localContentProvider; private readonly IUserCommandHandlerService _userCommandHandlerService; private readonly ITimelessContentProvider _timelessContentProvider; private readonly IUserCommunicationService _userCommunicationService; private readonly IFrequencyNavigationService _frequencyNavigationService; private readonly ICommandPaletteService _commandPaletteService; private readonly IContentProviderRegistry _contentProviderRegistry; private readonly ILogger _logger; private readonly ApplicationConfiguration _applicationConfiguration; private ITabViewModel? _selectedTab; private IDeclarativeProperty? _currentLocation; private IDeclarativeProperty? _currentSelectedItem; private IDeclarativeProperty?>? _currentItems; private ViewMode _viewMode; public NavigationUserCommandHandlerService( IAppState appState, IServiceProvider serviceProvider, ILocalContentProvider localContentProvider, IUserCommandHandlerService userCommandHandlerService, ITimelessContentProvider timelessContentProvider, IUserCommunicationService userCommunicationService, IFrequencyNavigationService frequencyNavigationService, ICommandPaletteService commandPaletteService, IContentProviderRegistry contentProviderRegistry, ILogger logger, ApplicationConfiguration applicationConfiguration) : base(appState) { _appState = appState; _serviceProvider = serviceProvider; _localContentProvider = localContentProvider; _userCommandHandlerService = userCommandHandlerService; _timelessContentProvider = timelessContentProvider; _userCommunicationService = userCommunicationService; _frequencyNavigationService = frequencyNavigationService; _commandPaletteService = commandPaletteService; _contentProviderRegistry = contentProviderRegistry; _logger = logger; _applicationConfiguration = applicationConfiguration; SaveSelectedTab(t => _selectedTab = t); SaveCurrentSelectedItem(i => _currentSelectedItem = i); SaveCurrentLocation(l => _currentLocation = l); SaveCurrentItems(i => _currentItems = i); appState.ViewMode.Subscribe(v => _viewMode = v); AddCommandHandler(new IUserCommandHandler[] { new TypeUserCommandHandler(CloseTab), new TypeUserCommandHandler(EnterRapidTravel), new TypeUserCommandHandler(ExitRapidTravel), new TypeUserCommandHandler(GoBack), new TypeUserCommandHandler(GoByFrequency), new TypeUserCommandHandler(GoForward), new TypeUserCommandHandler(GoToHome), new TypeUserCommandHandler(GoToPath), new TypeUserCommandHandler(GoToProvider), new TypeUserCommandHandler(GoToRoot), new TypeUserCommandHandler(GoUp), new TypeUserCommandHandler(MoveCursorDown), new TypeUserCommandHandler(MoveCursorDownPage), new TypeUserCommandHandler(MoveCursorToFirst), new TypeUserCommandHandler(MoveCursorToLast), new TypeUserCommandHandler(MoveCursorUp), new TypeUserCommandHandler(MoveCursorUpPage), new TypeUserCommandHandler(NewTabAsync), new TypeUserCommandHandler(OpenCommandPalette), new TypeUserCommandHandler(OpenContainer), new TypeUserCommandHandler(OpenSelected), new TypeUserCommandHandler(RunOrOpen), new TypeUserCommandHandler(Refresh), new TypeUserCommandHandler(SelectNextTab), new TypeUserCommandHandler(SelectPreviousTab), new TypeUserCommandHandler(SwitchToTab), }); } private async Task GoBack() { if (_selectedTab?.Tab is null) return; await _selectedTab.Tab.GoBackAsync(); } private async Task GoForward() { if (_selectedTab?.Tab is null) return; await _selectedTab.Tab.GoForwardAsync(); } private async Task RunOrOpen(RunOrOpenCommand command) { var item = command.Item ?? _currentSelectedItem?.Value; if (item is IContainerViewModel) { await OpenSelected(); } else if ( item is IElementViewModel { Element: {NativePath: not null, Provider: ILocalContentProvider} localFile } ) { var processStartInfo = new ProcessStartInfo(localFile.NativePath!.Path) {UseShellExecute = true}; Process.Start(processStartInfo); if (_viewMode == ViewMode.RapidTravel) { await ExitRapidTravel(); } } } private Task OpenCommandPalette() { _commandPaletteService.OpenCommandPalette(); return Task.CompletedTask; } private async Task GoByFrequency() { await _frequencyNavigationService.OpenNavigationWindow(); } private async Task GoToPath() { var pathInput = new TextInputElement("Path"); var acceptedForm = await _userCommunicationService.ReadInputs(pathInput); if (!acceptedForm) return; var path = pathInput.Value!; IItem? resolvedPath = null; try { resolvedPath = await _timelessContentProvider.GetItemByNativePathAsync(new NativePath(path)); } catch { } if (resolvedPath is IContainer container) { await _userCommandHandlerService.HandleCommandAsync( new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, container))); } else if (resolvedPath is IElement element) { await _userCommandHandlerService.HandleCommandAsync( new OpenContainerCommand(element.Parent!)); } else { await _userCommunicationService.ShowMessageBox( $"Path does not exists: {path}", okText: "Ok", showCancel: false ); } } private async Task GoToHome() { var path = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var resolvedPath = await _localContentProvider.GetItemByNativePathAsync(new NativePath(path), PointInTime.Present); if (resolvedPath is IContainer homeFolder) { await _userCommandHandlerService.HandleCommandAsync( new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, homeFolder))); } } private async Task GoToRoot() { var root = _currentLocation?.Value; if (root is null) return; while (true) { var parent = root.Parent; if (parent is null || string.IsNullOrWhiteSpace(parent.Path.Path)) break; if (await parent.ResolveAsync() is not IContainer next) break; root = next; } await _userCommandHandlerService.HandleCommandAsync( new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, root))); } private async Task GoToProvider() { if (_currentLocation?.Value is null) return; await _userCommandHandlerService.HandleCommandAsync( new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, _currentLocation.Value.Provider))); } private async Task Refresh() { if (_currentLocation?.Value?.FullName is null) return; var refreshedItem = await _timelessContentProvider.GetItemByFullNameAsync(_currentLocation.Value.FullName, PointInTime.Present); if (refreshedItem is not IContainer refreshedContainer) return; if (_selectedTab?.Tab is { } tab) { await tab.ForceSetCurrentLocation(refreshedContainer); } } private async Task OpenContainer(OpenContainerCommand command) { var resolvedPath = await command.Path.ResolveAsync(); if (resolvedPath is not IContainer resolvedContainer) return; if (_selectedTab?.Tab is { } tab) { await tab.SetCurrentLocation(resolvedContainer); } } private async Task OpenSelected() { var targetContainer = await GetSubContainer(); if (targetContainer is null && _currentSelectedItem?.Value is IContainerViewModel {Container: { } container}) { targetContainer = container; } if (targetContainer is null) { return; } await _appState.SetRapidTravelTextAsync(""); if (_selectedTab?.Tab is { } tab) { await tab.SetCurrentLocation(targetContainer); } async Task GetSubContainer() { if (_currentSelectedItem?.Value is not {BaseItem: IElement element}) { return null; } var subContentProvider = await _contentProviderRegistry.GetSubContentProviderForElement(element); if (subContentProvider is null) return null; var resolvedItem = await subContentProvider.GetItemByFullNameAsync( element, new FullName(""), _timelessContentProvider.CurrentPointInTime.Value!); return resolvedItem as IContainer; } } private async Task GoUp() { if (_currentLocation?.Value?.Parent is not { } parentPath || await parentPath.ResolveAsyncSafe() is not IContainer newContainer) { return; } await _appState.SetRapidTravelTextAsync(""); if (_selectedTab?.Tab is { } tab) { await tab.SetCurrentLocation(newContainer); } } private async Task MoveCursorDown() => await SelectNewSelectedItem(items => { if (_currentSelectedItem?.Value == null) return items.FirstOrDefault(); return items.SkipWhile(i => !i.EqualsTo(_currentSelectedItem?.Value)).Skip(1).FirstOrDefault(); }); private async Task MoveCursorUp() => await SelectNewSelectedItem(items => { if (_currentSelectedItem?.Value == null) return items.LastOrDefault(); return items.TakeWhile(i => !i.EqualsTo(_currentSelectedItem?.Value)).LastOrDefault(); }); private async Task MoveCursorDownPage() => await SelectNewSelectedItem(items => { var relevantItems = _currentSelectedItem?.Value is null ? items.ToList() : items.SkipWhile(i => !i.EqualsTo(_currentSelectedItem.Value)).ToList(); var fallBackItems = relevantItems.Take(PageSize + 1).Reverse(); var preferredItems = relevantItems.Skip(PageSize + 1); return preferredItems.Concat(fallBackItems).FirstOrDefault(); }); private async Task MoveCursorUpPage() => await SelectNewSelectedItem(items => { var relevantItems = _currentSelectedItem?.Value is null ? items.Reverse().ToList() : items.TakeWhile(i => !i.EqualsTo(_currentSelectedItem?.Value)).Reverse().ToList(); var fallBackItems = relevantItems.Take(PageSize).Reverse(); var preferredItems = relevantItems.Skip(PageSize); return preferredItems.Concat(fallBackItems).FirstOrDefault(); }); private async Task MoveCursorToFirst() => await SelectNewSelectedItem(items => items.FirstOrDefault()); private async Task MoveCursorToLast() => await SelectNewSelectedItem(items => items.LastOrDefault()); private Task SelectNewSelectedItem(Func, IItemViewModel?> getNewSelected) { if (_selectedTab is null || _currentItems?.Value is null) return Task.CompletedTask; var newSelectedItem = getNewSelected(_currentItems.Value.ToArray()); if (newSelectedItem == null) return Task.CompletedTask; if (_selectedTab.Tab is { } tab) { tab.SetSelectedItem(newSelectedItem.ToAbsolutePath(_timelessContentProvider)); } return Task.CompletedTask; } private Task EnterRapidTravel() { _appState.SwitchViewModeAsync(ViewMode.RapidTravel); return Task.CompletedTask; } private Task ExitRapidTravel() { _appState.SwitchViewModeAsync(ViewMode.Default); return Task.CompletedTask; } private async Task SwitchToTab(SwitchToTabCommand command) { var tabNumber = command.TabNumber; var tabViewModel = _appState.Tabs.FirstOrDefault(t => t.TabNumber == tabNumber); if (tabNumber == -1) { var greatestNumber = _appState.Tabs.Max(t => t.TabNumber); tabViewModel = _appState.Tabs.FirstOrDefault(t => t.TabNumber == greatestNumber); } if (tabViewModel == null) { var newLocation = await GetLocationForNewTabAsync(tabNumber); tabViewModel = await CreateTabAsync(newLocation, tabNumber); } if (_viewMode == ViewMode.RapidTravel) { await _userCommandHandlerService.HandleCommandAsync(ExitRapidTravelCommand.Instance); } await _appState.SetSelectedTabAsync(tabViewModel); } private async Task NewTabAsync(NewTabCommand command) { var numbers = _appState.Tabs.Select(t => t.TabNumber).ToHashSet(); var tabNumber = 1; while (numbers.Contains(tabNumber)) { tabNumber++; } IContainer? newLocation = null; if (command.Path is { } path) { newLocation = await _timelessContentProvider.GetItemByFullNameAsync(path, PointInTime.Present) as IContainer; } newLocation ??= await GetLocationForNewTabAsync(tabNumber); var tabViewModel = await CreateTabAsync(newLocation, tabNumber); if (_viewMode == ViewMode.RapidTravel) { await _userCommandHandlerService.HandleCommandAsync(ExitRapidTravelCommand.Instance); } if (command.Open) { await _appState.SetSelectedTabAsync(tabViewModel); } } private async Task SelectNextTab() { var currentTabNumber = _appState.SelectedTab.Value?.TabNumber; var nextTabNumbers = _appState.Tabs.Select(t => t.TabNumber).Order().SkipWhile(n => n <= currentTabNumber).ToArray(); if (nextTabNumbers.Length == 0) return; var nextTabNumber = nextTabNumbers[0]; var tabViewModel = _appState.Tabs.FirstOrDefault(t => t.TabNumber == nextTabNumber); await _appState.SetSelectedTabAsync(tabViewModel!); } private async Task SelectPreviousTab() { var currentTabNumber = _appState.SelectedTab.Value?.TabNumber; var nextTabNumbers = _appState.Tabs.Select(t => t.TabNumber).Order().TakeWhile(n => n < currentTabNumber).ToArray(); if (nextTabNumbers.Length == 0) return; var nextTabNumber = nextTabNumbers[^1]; var tabViewModel = _appState.Tabs.FirstOrDefault(t => t.TabNumber == nextTabNumber); await _appState.SetSelectedTabAsync(tabViewModel!); } private async Task GetLocationForNewTabAsync(int tabNumber) { try { var newLocation = _currentLocation?.Value?.FullName is { } fullName ? (IContainer) await _timelessContentProvider.GetItemByFullNameAsync(fullName, PointInTime.Present) : _localContentProvider; return newLocation; } catch (Exception ex) { var fullName = _currentLocation?.Value?.FullName?.Path ?? "unknown"; _logger.LogError(ex, "Could not resolve container while switching to tab {TabNumber} to path {FullName}", tabNumber, fullName); } return _localContentProvider; } private async Task CreateTabAsync(IContainer newLocation, int tabNumber) { var tab = await _serviceProvider.GetAsyncInitableResolver(newLocation) .GetRequiredServiceAsync(); var newTabViewModel = _serviceProvider.GetInitableResolver(tab, tabNumber).GetRequiredService(); _appState.AddTab(newTabViewModel); return newTabViewModel; } private Task CloseTab() { if ((!_applicationConfiguration.AllowCloseLastTab && _appState.Tabs.Count < 2) || _selectedTab == null) return Task.CompletedTask; var tabToRemove = _selectedTab; _appState.RemoveTab(tabToRemove!); try { tabToRemove.Dispose(); } catch { } return Task.CompletedTask; } }