diff --git a/src/AppCommon/FileTime.App.Core/Services/Persistence/TabPersistenceService.cs b/src/AppCommon/FileTime.App.Core/Services/Persistence/TabPersistenceService.cs index 03d8bb6..17da0a2 100644 --- a/src/AppCommon/FileTime.App.Core/Services/Persistence/TabPersistenceService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/Persistence/TabPersistenceService.cs @@ -15,32 +15,17 @@ public class TabPersistenceService : ITabPersistenceService private readonly IAppState _appState; private readonly ILogger _logger; - private class PersistenceRoot + //TODO: make this a configuration maybe? + private readonly List _contentProvidersNotToRestore = new() { - public TabStates? TabStates { get; set; } - } + "search" + }; - private class TabStates - { - public List? Tabs { get; set; } - public int? ActiveTabNumber { get; set; } - } + private record PersistenceRoot(TabStates? TabStates); - private class TabState - { - public string? Path { get; set; } - public int Number { get; set; } + private record TabStates(List? Tabs, int? ActiveTabNumber); - public TabState() - { - } - - public TabState(FullName path, int number) - { - Path = path.Path; - Number = number; - } - } + private record TabState(string? Path, int Number); private readonly string _settingsPath; private readonly JsonSerializerOptions _jsonOptions; @@ -149,7 +134,7 @@ public class TabPersistenceService : ITabPersistenceService } catch { - path = path.GetParent(); + path = path?.GetParent(); if (path == null) { throw new Exception($"Could not find an initializable path along {tab.Path}"); @@ -159,6 +144,8 @@ public class TabPersistenceService : ITabPersistenceService if (container == null) continue; + if (_contentProvidersNotToRestore.Contains(container.Provider.Name)) continue; + var tabToLoad = await _serviceProvider.GetAsyncInitableResolver(container) .GetRequiredServiceAsync(); var tabViewModel = _serviceProvider.GetInitableResolver(tabToLoad, tab.Number) @@ -181,7 +168,15 @@ public class TabPersistenceService : ITabPersistenceService if (_appState.Tabs.Count == 0) return false; - var tabToActivate = _appState.Tabs.FirstOrDefault(t => t.TabNumber == tabStates.ActiveTabNumber); + var optimalTabs = _appState + .Tabs + .TakeWhile(t => t.TabNumber <= tabStates.ActiveTabNumber) + .Reverse(); + var suboptimalTabs = _appState + .Tabs + .SkipWhile(t => t.TabNumber <= tabStates.ActiveTabNumber); + + var tabToActivate = optimalTabs.Concat(suboptimalTabs).FirstOrDefault(); if (tabToActivate is not null) _appState.SetSelectedTab(tabToActivate); return true; @@ -189,10 +184,8 @@ public class TabPersistenceService : ITabPersistenceService public void SaveStates(CancellationToken token = default) { - var state = new PersistenceRoot - { - TabStates = SerializeTabStates() - }; + var state = new PersistenceRoot(SerializeTabStates()); + var settingsDirectory = new DirectoryInfo(string.Join(Path.DirectorySeparatorChar, _settingsPath.Split(Path.DirectorySeparatorChar)[0..^1])); if (!settingsDirectory.Exists) settingsDirectory.Create(); @@ -207,16 +200,15 @@ public class TabPersistenceService : ITabPersistenceService { var currentLocation = tab.CurrentLocation.Value; if (currentLocation is null) continue; - tabStates.Add(new TabState(currentLocation.FullName!, tab.TabNumber)); + tabStates.Add(new TabState(currentLocation.FullName!.Path, tab.TabNumber)); } - return new TabStates - { - Tabs = tabStates, - ActiveTabNumber = _appState.CurrentSelectedTab?.TabNumber - }; + return new TabStates( + tabStates, + _appState.CurrentSelectedTab?.TabNumber + ); } - public async Task InitAsync() + public async Task InitAsync() => await LoadStatesAsync(); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs index 545e2be..3b9d3b9 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs @@ -10,7 +10,6 @@ using FileTime.App.Core.UserCommand; using FileTime.App.Core.ViewModels; using FileTime.Core.Command; using FileTime.Core.Command.CreateContainer; -using FileTime.Core.Command.CreateElement; using FileTime.Core.Command.Move; using FileTime.Core.Extensions; using FileTime.Core.Interactions; diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ToolUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ToolUserCommandHandlerService.cs index 2a1e92a..25154c6 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ToolUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ToolUserCommandHandlerService.cs @@ -15,7 +15,7 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase { private readonly ISystemClipboardService _systemClipboardService; private readonly IUserCommunicationService _userCommunicationService; - private readonly ISearchManager _searchManager; + private readonly ISearchContentProvider _searchContentProvider; private readonly IItemNameConverterService _itemNameConverterService; private readonly ITimelessContentProvider _timelessContentProvider; private readonly IUserCommandHandlerService _userCommandHandlerService; @@ -28,7 +28,7 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase IAppState appState, ISystemClipboardService systemClipboardService, IUserCommunicationService userCommunicationService, - ISearchManager searchManager, + ISearchContentProvider searchContentProvider, IItemNameConverterService itemNameConverterService, ITimelessContentProvider timelessContentProvider, IUserCommandHandlerService userCommandHandlerService, @@ -36,7 +36,7 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase { _systemClipboardService = systemClipboardService; _userCommunicationService = userCommunicationService; - _searchManager = searchManager; + _searchContentProvider = searchContentProvider; _itemNameConverterService = itemNameConverterService; _timelessContentProvider = timelessContentProvider; _userCommandHandlerService = userCommandHandlerService; @@ -113,7 +113,7 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase _ => throw new ArgumentOutOfRangeException() }; - var searchTask = await _searchManager.StartSearchAsync(searchMatcher, _currentLocation.Value); + var searchTask = await _searchContentProvider.StartSearchAsync(searchMatcher, _currentLocation.Value); var openContainerCommand = new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, searchTask.SearchContainer)); await _userCommandHandlerService.HandleCommandAsync(openContainerCommand); } diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs index 3d18d8f..12e3010 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs @@ -52,7 +52,7 @@ public abstract partial class ItemViewModel : IItemViewModel var displayName = itemViewModelType switch { ItemViewModelType.Main => _appState.RapidTravelText.Map(s => (IReadOnlyList) _itemNameConverterService.GetDisplayName(item.DisplayName, s)), - _ => new DeclarativeProperty>(new List {new (item.DisplayName)}), + _ => new DeclarativeProperty>(new List {new(item.DisplayName)}), }; BaseItem = item; @@ -103,6 +103,6 @@ public abstract partial class ItemViewModel : IItemViewModel var deepestPath = _parentTab.Tab.LastDeepestSelectedPath; var commonPath = FullName.CreateSafe(PathHelper.GetCommonPath(ownFullName.Path, deepestPath.Path)); - return commonPath.Path == ownFullName.Path; + return commonPath is not null && commonPath.Path == ownFullName.Path; } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs index d1f5b77..372f106 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs @@ -198,7 +198,7 @@ public partial class TabViewModel : ITabViewModel private static async Task MapItemAsync(AbsolutePath item) => await item.ResolveAsync(forceResolve: true, - itemInitializationSettings: new ItemInitializationSettings(true)); + itemInitializationSettings: new ItemInitializationSettings {SkipChildInitialization = true}); private IItemViewModel MapItemToViewModel(IItem item, ItemViewModelType type) { diff --git a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs index 24c287f..05c48cb 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs +++ b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs @@ -31,6 +31,7 @@ public static class DependencyInjection serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(); + serviceCollection.TryAddSingleton(); //TODO: check local/remote context serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(); diff --git a/src/AppCommon/FileTime.App.Search.Abstractions/ISearchContainerProvider.cs b/src/AppCommon/FileTime.App.Search.Abstractions/ISearchContainerProvider.cs index c093972..205d828 100644 --- a/src/AppCommon/FileTime.App.Search.Abstractions/ISearchContainerProvider.cs +++ b/src/AppCommon/FileTime.App.Search.Abstractions/ISearchContainerProvider.cs @@ -1,8 +1,10 @@ using FileTime.Core.ContentAccess; +using FileTime.Core.Models; namespace FileTime.App.Search; public interface ISearchContentProvider : IContentProvider { - + Task StartSearchAsync(ISearchMatcher matcher, IContainer searchIn); + void RemoveSearch(FullName searchFullName); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Search.Abstractions/ISearchManager.cs b/src/AppCommon/FileTime.App.Search.Abstractions/ISearchManager.cs deleted file mode 100644 index 75a54dd..0000000 --- a/src/AppCommon/FileTime.App.Search.Abstractions/ISearchManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -using FileTime.Core.Models; - -namespace FileTime.App.Search; - -public interface ISearchManager -{ - Task StartSearchAsync(ISearchMatcher matcher, IContainer searchIn); - IReadOnlyList SearchTasks { get; } -} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs b/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs index 3042dde..69b9f46 100644 --- a/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs +++ b/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs @@ -7,12 +7,14 @@ namespace FileTime.App.Search; public class SearchContentProvider : ContentProviderBase, ISearchContentProvider { - private readonly ISearchManager _searchManager; + private readonly ITimelessContentProvider _timelessContentProvider; + private readonly List _searchTasks = new(); public const string ContentProviderName = "search"; - public SearchContentProvider(ISearchManager searchManager) : base(ContentProviderName) + public SearchContentProvider(ITimelessContentProvider timelessContentProvider) + : base(ContentProviderName, timelessContentProvider) { - _searchManager = searchManager; + _timelessContentProvider = timelessContentProvider; } public override Task GetItemByNativePathAsync( @@ -21,9 +23,12 @@ public class SearchContentProvider : ContentProviderBase, ISearchContentProvider bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, ItemInitializationSettings itemInitializationSettings = default - ) => - Task.FromResult((IItem) _searchManager.SearchTasks + ) + { + if (nativePath.Path == ContentProviderName) return Task.FromResult((IItem) this); + return Task.FromResult((IItem) _searchTasks .First(searchTask => searchTask.SearchContainer.NativePath == nativePath).SearchContainer); + } public override NativePath GetNativePath(FullName fullName) => new(fullName.Path); public override FullName GetFullName(NativePath nativePath) => new(nativePath.Path); @@ -36,4 +41,34 @@ public class SearchContentProvider : ContentProviderBase, ISearchContentProvider => Task.FromResult(null as byte[]); public override bool CanHandlePath(NativePath path) => path.Path.StartsWith(ContentProviderName); + + public async Task StartSearchAsync(ISearchMatcher matcher, IContainer searchIn) + { + var searchTask = new SearchTask( + searchIn, + this, + _timelessContentProvider, + matcher, + new AbsolutePath(_timelessContentProvider, this) + ); + + _searchTasks.Add(searchTask); + await searchTask.StartAsync(); + Items.Add(new AbsolutePath(_timelessContentProvider, searchTask.SearchContainer)); + + return searchTask; + } + + public void RemoveSearch(FullName searchFullName) + { + var searchTask = _searchTasks.FirstOrDefault(t => t.SearchContainer.FullName == searchFullName); + if (searchTask is null) return; + + _searchTasks.Remove(searchTask); + var searchItem = Items.FirstOrDefault(c => c.Path == searchTask.SearchContainer.FullName); + if (searchItem is not null) + { + Items.Remove(searchItem); + } + } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Search/SearchItemDeleter.cs b/src/AppCommon/FileTime.App.Search/SearchItemDeleter.cs new file mode 100644 index 0000000..8281a46 --- /dev/null +++ b/src/AppCommon/FileTime.App.Search/SearchItemDeleter.cs @@ -0,0 +1,13 @@ +using FileTime.Core.ContentAccess; +using FileTime.Core.Models; + +namespace FileTime.App.Search; + +public class SearchItemDeleter : IItemDeleter +{ + public Task DeleteAsync(ISearchContentProvider contentProvider, FullName fullName) + { + contentProvider.RemoveSearch(fullName); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Search/SearchManager.cs b/src/AppCommon/FileTime.App.Search/SearchManager.cs deleted file mode 100644 index e217cd2..0000000 --- a/src/AppCommon/FileTime.App.Search/SearchManager.cs +++ /dev/null @@ -1,35 +0,0 @@ -using FileTime.Core.Models; -using Microsoft.Extensions.DependencyInjection; - -namespace FileTime.App.Search; - -public class SearchManager : ISearchManager -{ - private readonly IServiceProvider _serviceProvider; - private ISearchContentProvider? _searchContainerProvider; - private readonly List _searchTasks = new(); - - public IReadOnlyList SearchTasks { get; } - - public SearchManager(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - SearchTasks = _searchTasks.AsReadOnly(); - } - - public async Task StartSearchAsync(ISearchMatcher matcher, IContainer searchIn) - { - _searchContainerProvider ??= _serviceProvider.GetRequiredService(); - var searchTask = new SearchTask( - searchIn, - _searchContainerProvider, - matcher - ); - - _searchTasks.Add(searchTask); - - await searchTask.StartAsync(); - - return searchTask; - } -} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Search/SearchTask.cs b/src/AppCommon/FileTime.App.Search/SearchTask.cs index be2c324..c787712 100644 --- a/src/AppCommon/FileTime.App.Search/SearchTask.cs +++ b/src/AppCommon/FileTime.App.Search/SearchTask.cs @@ -10,6 +10,7 @@ namespace FileTime.App.Search; public class SearchTask : ISearchTask { private readonly IContainer _baseContainer; + private readonly ITimelessContentProvider _timelessContentProvider; private readonly ISearchMatcher _matcher; private readonly Container _container; private readonly ObservableCollection _exceptions = new(); @@ -22,19 +23,22 @@ public class SearchTask : ISearchTask public SearchTask( IContainer baseContainer, - IContentProvider contentProvider, - ISearchMatcher matcher + ISearchContentProvider contentProvider, + ITimelessContentProvider timelessContentProvider, + ISearchMatcher matcher, + AbsolutePath parent ) { var randomId = $"{SearchContentProvider.ContentProviderName}/{_searchId++}_{baseContainer.Name}"; _baseContainer = baseContainer; + _timelessContentProvider = timelessContentProvider; _matcher = matcher; _container = new Container( baseContainer.Name, baseContainer.DisplayName, new FullName(randomId), new NativePath(randomId), - null, + parent, false, true, null, @@ -85,7 +89,11 @@ public class SearchTask : ISearchTask foreach (var itemPath in items) { - var item = await itemPath.ResolveAsync(); + var item = await itemPath.ResolveAsync( + itemInitializationSettings: new ItemInitializationSettings + { + Parent = new AbsolutePath(_timelessContentProvider, _container) + }); if (await _matcher.IsItemMatchAsync(item)) { _items.Add(itemPath); diff --git a/src/AppCommon/FileTime.App.Search/Startup.cs b/src/AppCommon/FileTime.App.Search/Startup.cs index 2b75630..0f030d4 100644 --- a/src/AppCommon/FileTime.App.Search/Startup.cs +++ b/src/AppCommon/FileTime.App.Search/Startup.cs @@ -10,7 +10,8 @@ public static class Startup { services.TryAddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); - services.TryAddSingleton(); + services.TryAddSingleton, SearchItemDeleter>(); + services.TryAddSingleton>(sp => sp.GetRequiredService>()); return services; } diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProviderRegistry.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProviderRegistry.cs index a38f409..b152075 100644 --- a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProviderRegistry.cs +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProviderRegistry.cs @@ -1,8 +1,10 @@ +using System.Collections.ObjectModel; + namespace FileTime.Core.ContentAccess; public interface IContentProviderRegistry { - IEnumerable ContentProviders { get; } + ReadOnlyObservableCollection ContentProviders { get; } void AddContentProvider(IContentProvider contentProvider); void RemoveContentProvider(IContentProvider contentProvider); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IRootContentProvider.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IRootContentProvider.cs new file mode 100644 index 0000000..d9d3cb1 --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IRootContentProvider.cs @@ -0,0 +1,6 @@ +namespace FileTime.Core.ContentAccess; + +public interface IRootContentProvider : IContentProvider +{ + +} \ 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 11d0f09..9dde8e1 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/FullName.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/FullName.cs @@ -4,25 +4,29 @@ public record FullName(string Path) { public FullName? GetParent() { + if (Path.Length == 0) return null; var pathParts = Path.TrimEnd(Constants.SeparatorChar).Split(Constants.SeparatorChar); return pathParts.Length switch { > 1 => CreateSafe(string.Join(Constants.SeparatorChar, pathParts.SkipLast(1))), - _ => null + _ => CreateSafe("") }; } public static FullName? CreateSafe(string? path) { - if (string.IsNullOrWhiteSpace(path)) + if (path is null) return null; + if (string.IsNullOrWhiteSpace(path)) + return new FullName(""); + return new(path); } public string GetName() => Path.Split(Constants.SeparatorChar).Last(); - public FullName GetChild(string childName) + public FullName GetChild(string childName) => new(Path + Constants.SeparatorChar + childName); public override string ToString() => Path; diff --git a/src/Core/FileTime.Core.Abstraction/Models/ItemInitializationSettings.cs b/src/Core/FileTime.Core.Abstraction/Models/ItemInitializationSettings.cs index b44a70b..13c24ba 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/ItemInitializationSettings.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/ItemInitializationSettings.cs @@ -1,11 +1,7 @@ namespace FileTime.Core.Models; -public readonly struct ItemInitializationSettings +public sealed class ItemInitializationSettings { - public readonly bool SkipChildInitialization; - - public ItemInitializationSettings(bool skipChildInitialization) - { - SkipChildInitialization = skipChildInitialization; - } + public bool SkipChildInitialization { get; init; } + public AbsolutePath? Parent { get; init; } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Command/Delete/DeleteCommand.cs b/src/Core/FileTime.Core.Command/Delete/DeleteCommand.cs index 2209d4e..4c39e83 100644 --- a/src/Core/FileTime.Core.Command/Delete/DeleteCommand.cs +++ b/src/Core/FileTime.Core.Command/Delete/DeleteCommand.cs @@ -52,7 +52,10 @@ public class DeleteCommand : CommandBase, IExecutableCommand new DeleteStrategy() ); - var parents = ItemsToDelete.Select(i => i.GetParent()).OfType().Distinct(); + var parents = ItemsToDelete + .Select(i => i.GetParent()) + .OfType() + .Distinct(); foreach (var parent in parents) { await _commandSchedulerNotifier.RefreshContainer(parent); @@ -68,18 +71,26 @@ public class DeleteCommand : CommandBase, IExecutableCommand foreach (var itemToDeleteName in itemsToDelete) { var itemToDelete = await _timelessContentProvider.GetItemByFullNameAsync(itemToDeleteName, currentTime); - IItemDeleter itemDeleter; + IItemDeleter? itemDeleter = null; - if (itemDeleters.ContainsKey(itemToDelete.Provider.Name)) + if (itemDeleters.TryGetValue(itemToDelete.Provider.Name, out var deleter)) { - itemDeleter = itemDeleters[itemToDelete.Provider.Name]; + itemDeleter = deleter; } else { - itemDeleter = _contentAccessorFactory.GetItemDeleter(itemToDelete.Provider); - itemDeleters.Add(itemToDelete.Provider.Name, itemDeleter); + try + { + itemDeleter = _contentAccessorFactory.GetItemDeleter(itemToDelete.Provider); + itemDeleters.Add(itemToDelete.Provider.Name, itemDeleter); + } + catch + { + } } + if (itemDeleter is null) continue; + if (itemToDelete is IContainer container) { await TraverseTree( @@ -95,7 +106,7 @@ public class DeleteCommand : CommandBase, IExecutableCommand } } - await itemDeleter.DeleteAsync(itemToDelete.Provider, itemToDelete.FullName!); + await deleteStrategy.DeleteItem(itemToDelete, itemDeleter); } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Command/Delete/DeleteStrategy.cs b/src/Core/FileTime.Core.Command/Delete/DeleteStrategy.cs index 3fe1897..60f55a9 100644 --- a/src/Core/FileTime.Core.Command/Delete/DeleteStrategy.cs +++ b/src/Core/FileTime.Core.Command/Delete/DeleteStrategy.cs @@ -5,8 +5,6 @@ namespace FileTime.Core.Command.Delete; public class DeleteStrategy : IDeleteStrategy { - public async Task DeleteItem(IItem item, IItemDeleter deleter) - { - await deleter.DeleteAsync(item.Provider, item.FullName!); - } + public async Task DeleteItem(IItem item, IItemDeleter deleter) + => await deleter.DeleteAsync(item.Provider, item.FullName!); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs b/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs index 165417c..4a6e6a3 100644 --- a/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs +++ b/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs @@ -32,7 +32,7 @@ public abstract class ContentProviderBase : IContentProvider public IContentProvider Provider => this; - public AbsolutePath? Parent => null; + public AbsolutePath? Parent { get; } public DateTime? CreatedAt => null; @@ -46,14 +46,15 @@ public abstract class ContentProviderBase : IContentProvider public bool AllowRecursiveDeletion => false; public AbsolutePathType Type => AbsolutePathType.Container; - public PointInTime PointInTime { get; } = PointInTime.Eternal; + public PointInTime PointInTime => PointInTime.Eternal; public ObservableCollection Exceptions { get; } = new(); ReadOnlyExtensionCollection IItem.Extensions => _extensions; - protected ContentProviderBase(string name) + protected ContentProviderBase(string name, ITimelessContentProvider timelessContentProvider) { + Parent = new AbsolutePath(timelessContentProvider, PointInTime.Eternal, new FullName(""), AbsolutePathType.Container); DisplayName = Name = name; FullName = FullName.CreateSafe(name); Extensions = new ExtensionCollection(); diff --git a/src/Core/FileTime.Core.ContentAccess/ContentProviderRegistry.cs b/src/Core/FileTime.Core.ContentAccess/ContentProviderRegistry.cs index b9602db..6d02442 100644 --- a/src/Core/FileTime.Core.ContentAccess/ContentProviderRegistry.cs +++ b/src/Core/FileTime.Core.ContentAccess/ContentProviderRegistry.cs @@ -1,21 +1,64 @@ +using System.Collections.ObjectModel; using Microsoft.Extensions.DependencyInjection; namespace FileTime.Core.ContentAccess; public class ContentProviderRegistry : IContentProviderRegistry { + private readonly object _lock = new(); private readonly IServiceProvider _serviceProvider; - private readonly Lazy> _defaultContentProviders; - private readonly List _additionalContentProviders = new(); + private readonly ObservableCollection _contentProviders = new(); + private readonly ReadOnlyObservableCollection _contentProvidersReadOnly; + private bool _initialized; public ContentProviderRegistry(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; - _defaultContentProviders = new Lazy>(() => serviceProvider.GetServices().ToList()); + _contentProvidersReadOnly = new ReadOnlyObservableCollection(_contentProviders); } - public IEnumerable ContentProviders => _defaultContentProviders.Value.Concat(_additionalContentProviders); + public ReadOnlyObservableCollection ContentProviders + { + get + { + InitializeContentProviderListIfNeeded(); + return _contentProvidersReadOnly; + } + } - public void AddContentProvider(IContentProvider contentProvider) => _additionalContentProviders.Add(contentProvider); - public void RemoveContentProvider(IContentProvider contentProvider) => _additionalContentProviders.Remove(contentProvider); + private void InitializeContentProviderListIfNeeded() + { + lock (_lock) + { + if (!_initialized) + { + foreach (var contentProvider in _serviceProvider.GetServices()) + { + _contentProviders.Add(contentProvider); + } + + _initialized = true; + } + } + } + + public void AddContentProvider(IContentProvider contentProvider) + { + InitializeContentProviderListIfNeeded(); + + lock (_lock) + { + _contentProviders.Add(contentProvider); + } + } + + public void RemoveContentProvider(IContentProvider contentProvider) + { + InitializeContentProviderListIfNeeded(); + + lock (_lock) + { + _contentProviders.Remove(contentProvider); + } + } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.ContentAccess/RootContentProvider.cs b/src/Core/FileTime.Core.ContentAccess/RootContentProvider.cs new file mode 100644 index 0000000..821e8cb --- /dev/null +++ b/src/Core/FileTime.Core.ContentAccess/RootContentProvider.cs @@ -0,0 +1,87 @@ +using System.Collections.ObjectModel; +using System.Reactive.Linq; +using FileTime.Core.Enums; +using FileTime.Core.Models; +using FileTime.Core.Timeline; +using ObservableComputations; + +namespace FileTime.Core.ContentAccess; + +public class RootContentProvider : IRootContentProvider +{ + private readonly ITimelessContentProvider _timelessContentProvider; + public string Name => "root"; + public string DisplayName => "Root"; + public FullName? FullName => null; + public NativePath? NativePath => null; + public AbsolutePath? Parent => null; + public bool IsHidden => false; + public bool IsExists => true; + public DateTime? CreatedAt => null; + public SupportsDelete CanDelete => SupportsDelete.False; + public bool CanRename => false; + public IContentProvider Provider => this; + public string? Attributes => null; + public AbsolutePathType Type => AbsolutePathType.Container; + public PointInTime PointInTime => PointInTime.Eternal; + public ObservableCollection Exceptions { get; } = new(); + public ReadOnlyExtensionCollection Extensions { get; } = new(new ExtensionCollection()); + public ObservableCollection Items { get; } + public IObservable IsLoading => Observable.Return(false); + public bool? IsLoaded => true; + public Task WaitForLoaded(CancellationToken token = default) => Task.CompletedTask; + + public bool AllowRecursiveDeletion => false; + public Task OnEnter() => Task.CompletedTask; + + public bool SupportsContentStreams => false; + + public RootContentProvider( + IContentProviderRegistry contentProviderRegistry, + ITimelessContentProvider timelessContentProvider) + { + _timelessContentProvider = timelessContentProvider; + Items = contentProviderRegistry + .ContentProviders + .Selecting(c => + new AbsolutePath(timelessContentProvider, c) + ); + } + + public async Task GetItemByFullNameAsync( + FullName fullName, + PointInTime pointInTime, + bool forceResolve = false, + AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, + ItemInitializationSettings itemInitializationSettings = default) + => await _timelessContentProvider.GetItemByFullNameAsync( + fullName, + pointInTime, + forceResolve, + forceResolvePathType, + itemInitializationSettings); + + public async Task GetItemByNativePathAsync( + NativePath nativePath, + PointInTime pointInTime, + bool forceResolve = false, + AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, + ItemInitializationSettings itemInitializationSettings = default) + => await _timelessContentProvider.GetItemByNativePathAsync( + nativePath, + pointInTime + ) ?? throw new FileNotFoundException(); + + public NativePath GetNativePath(FullName fullName) => throw new NotImplementedException(); + + public FullName GetFullName(NativePath nativePath) => throw new NotImplementedException(); + + public Task GetContentAsync( + IElement element, + int? maxLength = null, + CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public bool CanHandlePath(NativePath path) => throw new NotImplementedException(); + + public bool CanHandlePath(FullName path) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Services/Tab.cs b/src/Core/FileTime.Core.Services/Tab.cs index a405677..41fda98 100644 --- a/src/Core/FileTime.Core.Services/Tab.cs +++ b/src/Core/FileTime.Core.Services/Tab.cs @@ -140,7 +140,7 @@ public class Tab : ITab { var itemNameToSelect = LastDeepestSelectedPath.Path .Split(Constants.SeparatorChar) - .Skip(parentPath.Split(Constants.SeparatorChar).Length) + .Skip(parentPath == "" ? 0 : parentPath.Split(Constants.SeparatorChar).Length) .FirstOrDefault(); var itemToSelect = items.FirstOrDefault(i => i.FullName?.GetName() == itemNameToSelect); diff --git a/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs b/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs index 27f8465..fb1b6aa 100644 --- a/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs +++ b/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs @@ -2,18 +2,24 @@ using System.Reactive.Subjects; using FileTime.Core.ContentAccess; using FileTime.Core.Enums; using FileTime.Core.Models; +using Microsoft.Extensions.DependencyInjection; namespace FileTime.Core.Timeline; public class TimelessContentProvider : ITimelessContentProvider { private readonly IContentProviderRegistry _contentProviderRegistry; + private readonly Lazy _rootContentProvider; public BehaviorSubject CurrentPointInTime { get; } = new(PointInTime.Present); - public TimelessContentProvider(IContentProviderRegistry contentProviderRegistry) + public TimelessContentProvider( + IContentProviderRegistry contentProviderRegistry, + IServiceProvider serviceProvider + ) { _contentProviderRegistry = contentProviderRegistry; + _rootContentProvider = new Lazy(serviceProvider.GetRequiredService); } public async Task GetItemByFullNameAsync(FullName fullName, PointInTime? pointInTime, @@ -23,6 +29,8 @@ public class TimelessContentProvider : ITimelessContentProvider { //TODO time modifications var contentProviderName = fullName.Path.Split(Constants.SeparatorChar).FirstOrDefault(); + if (contentProviderName == "") return _rootContentProvider.Value; + var contentProvider = _contentProviderRegistry.ContentProviders.FirstOrDefault(p => p.Name == contentProviderName); if (contentProvider is null) @@ -37,7 +45,7 @@ public class TimelessContentProvider : ITimelessContentProvider { foreach (var contentProvider in _contentProviderRegistry.ContentProviders) { - if(!contentProvider.CanHandlePath(nativePath)) continue; + if (!contentProvider.CanHandlePath(nativePath)) continue; return await contentProvider.GetItemByNativePathAsync(nativePath, pointInTime ?? PointInTime.Present); } @@ -49,7 +57,7 @@ public class TimelessContentProvider : ITimelessContentProvider { foreach (var contentProvider in _contentProviderRegistry.ContentProviders) { - if(!contentProvider.CanHandlePath(nativePath)) continue; + if (!contentProvider.CanHandlePath(nativePath)) continue; return contentProvider.GetFullName(nativePath); } @@ -61,7 +69,7 @@ public class TimelessContentProvider : ITimelessContentProvider { foreach (var contentProvider in _contentProviderRegistry.ContentProviders) { - if(!contentProvider.CanHandlePath(fullName)) continue; + if (!contentProvider.CanHandlePath(fullName)) continue; return contentProvider.GetNativePath(fullName); } diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs index 41c7175..d31e3d7 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -14,7 +14,8 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo private readonly ITimelessContentProvider _timelessContentProvider; private readonly bool _isCaseInsensitive; - public LocalContentProvider(ITimelessContentProvider timelessContentProvider) : base(LocalContentProviderConstants.ContentProviderId) + public LocalContentProvider(ITimelessContentProvider timelessContentProvider) + : base(LocalContentProviderConstants.ContentProviderId, timelessContentProvider) { _timelessContentProvider = timelessContentProvider; _isCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); @@ -66,13 +67,13 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, - ItemInitializationSettings itemInitializationSettings = default) + ItemInitializationSettings? itemInitializationSettings = null) { var path = nativePath.Path; Exception? innerException; try { - if ((path?.Length ?? 0) == 0) + if (path.Length == 0) { return Task.FromResult((IItem) this); } @@ -81,7 +82,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo return Task.FromResult((IItem) DirectoryToContainer( new DirectoryInfo(path!.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar), pointInTime, - !itemInitializationSettings.SkipChildInitialization) + itemInitializationSettings) ); } else if (File.Exists(path)) @@ -193,18 +194,21 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo } private Container DirectoryToContainer(DirectoryInfo directoryInfo, PointInTime pointInTime, - bool initializeChildren = true) + ItemInitializationSettings? initializationSettings = null) { + initializationSettings ??= new(); + var fullName = GetFullName(directoryInfo.FullName); var parentFullName = fullName.GetParent(); var parent = - parentFullName is null + initializationSettings.Parent + ?? (parentFullName is null ? null : new AbsolutePath( _timelessContentProvider, pointInTime, parentFullName, - AbsolutePathType.Container); + AbsolutePathType.Container)); var exceptions = new ObservableCollection(); var children = new ObservableCollection(); @@ -229,7 +233,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo children ); - if (initializeChildren) + if (!initializationSettings.SkipChildInitialization) { Task.Run(async () => await LoadChildren(container, directoryInfo, children, pointInTime, exceptions)); } diff --git a/src/Providers/FileTime.Providers.LocalAdmin/AdminContentProvider.cs b/src/Providers/FileTime.Providers.LocalAdmin/AdminContentProvider.cs index 87d9528..5eae2c4 100644 --- a/src/Providers/FileTime.Providers.LocalAdmin/AdminContentProvider.cs +++ b/src/Providers/FileTime.Providers.LocalAdmin/AdminContentProvider.cs @@ -9,22 +9,22 @@ namespace FileTime.Providers.LocalAdmin; //TODO: this should be a RemoteContentProvider if there will be one public class AdminContentProvider : RemoteContentProvider, IAdminContentProvider { - public AdminContentProvider() : base("local", "localAdmin") + public AdminContentProvider(ITimelessContentProvider timelessContentProvider) : base(timelessContentProvider, "local", "localAdmin") { } - public override Task GetItemByNativePathAsync(NativePath nativePath, PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, ItemInitializationSettings itemInitializationSettings = default) + public override Task GetItemByNativePathAsync(NativePath nativePath, PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, ItemInitializationSettings itemInitializationSettings = default) => throw new NotImplementedException(); - public override NativePath GetNativePath(FullName fullName) + public override NativePath GetNativePath(FullName fullName) => throw new NotImplementedException(); - public override FullName GetFullName(NativePath nativePath) + public override FullName GetFullName(NativePath nativePath) => throw new NotImplementedException(); - public override Task GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default) + public override Task GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public override bool CanHandlePath(NativePath path) + public override bool CanHandlePath(NativePath path) => throw new NotImplementedException(); } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Remote/RemoteContentProvider.cs b/src/Providers/FileTime.Providers.Remote/RemoteContentProvider.cs index d40a89e..d4b835d 100644 --- a/src/Providers/FileTime.Providers.Remote/RemoteContentProvider.cs +++ b/src/Providers/FileTime.Providers.Remote/RemoteContentProvider.cs @@ -7,7 +7,11 @@ namespace FileTime.Providers.Remote; public class RemoteContentProvider : ContentProviderBase, IRemoteContentProvider { - public RemoteContentProvider(string remoteName, string name = "remote") : base(name) + public RemoteContentProvider( + ITimelessContentProvider timelessContentProvider, + string remoteName, + string name = "remote") + : base(name, timelessContentProvider) { }