diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IRxSchedulerService.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IRxSchedulerService.cs new file mode 100644 index 0000000..e9310bf --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IRxSchedulerService.cs @@ -0,0 +1,10 @@ +using System.Reactive.Concurrency; + +namespace FileTime.App.Core.Services +{ + public interface IRxSchedulerService + { + IScheduler GetWorkerScheduler(); + IScheduler GetUIScheduler(); + } +} \ 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 5f33c20..ef21606 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs @@ -15,5 +15,6 @@ namespace FileTime.App.Core.ViewModels IObservable> CurrentItems { get; } IObservable> MarkedItems { get; } IObservable?> SelectedsChildren { get; } + IObservable?> ParentsChildren { get; } } } \ 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 843404d..9e4e2df 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs @@ -1,9 +1,8 @@ +using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Reactive.Subjects; using FileTime.App.Core.Extensions; -using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Services; -using FileTime.Core.Enums; using FileTime.Core.Models; using FileTime.Core.Services; using Microsoft.Extensions.DependencyInjection; @@ -15,6 +14,7 @@ namespace FileTime.App.Core.ViewModels private readonly IServiceProvider _serviceProvider; private readonly IItemNameConverterService _itemNameConverterService; private readonly IAppState _appState; + private readonly IRxSchedulerService _rxSchedulerService; private readonly BehaviorSubject> _markedItems = new(Enumerable.Empty()); private readonly List _disposables = new(); private bool disposed; @@ -29,11 +29,13 @@ namespace FileTime.App.Core.ViewModels public IObservable> CurrentItems { get; private set; } = null!; public IObservable> MarkedItems { get; } public IObservable?> SelectedsChildren { get; private set; } = null!; + public IObservable?> ParentsChildren { get; private set; } = null!; public TabViewModel( IServiceProvider serviceProvider, IItemNameConverterService itemNameConverterService, - IAppState appState) + IAppState appState, + IRxSchedulerService rxSchedulerService) { _serviceProvider = serviceProvider; _itemNameConverterService = itemNameConverterService; @@ -41,6 +43,7 @@ namespace FileTime.App.Core.ViewModels MarkedItems = _markedItems.Select(e => e.ToList()).AsObservable(); IsSelected = _appState.SelectedTab.Select(s => s == this); + _rxSchedulerService = rxSchedulerService; } public void Init(ITab tab, int tabNumber) @@ -49,7 +52,16 @@ namespace FileTime.App.Core.ViewModels TabNumber = tabNumber; CurrentLocation = tab.CurrentLocation.AsObservable(); - CurrentItems = tab.CurrentItems.Select(items => items.Select(MapItemToViewModel).ToList()).Publish(new List()).RefCount(); + CurrentItems = tab.CurrentItems + .Select(items => + items == null + ? new List() + : items.Select(MapItemToViewModel).ToList()) + .ObserveOn(_rxSchedulerService.GetWorkerScheduler()) + .SubscribeOn(_rxSchedulerService.GetUIScheduler()) + .Publish(new List()) + .RefCount(); + CurrentSelectedItem = Observable.CombineLatest( CurrentItems, @@ -73,7 +85,36 @@ namespace FileTime.App.Core.ViewModels currentSelectedItemThrottled .Where(c => c is null || c is not IContainerViewModel) .Select(_ => (IReadOnlyList?)null) - ); + ) + .ObserveOn(_rxSchedulerService.GetWorkerScheduler()) + .SubscribeOn(_rxSchedulerService.GetUIScheduler()) + .Publish(null) + .RefCount(); + + var parentThrottled = CurrentLocation + .Select(l => l?.Parent) + .DistinctUntilChanged() + .Publish(null) + .RefCount(); + + ParentsChildren = Observable.Merge( + parentThrottled + .Where(p => p is not null) + .Select(p => Observable.FromAsync(async () => (IContainer)await p!.ResolveAsync())) + .Switch() + .Select(p => p.Items) + .Switch() + .Select(items => Observable.FromAsync(async () => await Map(items))) + .Switch() + .Select(items => items?.Select(MapItemToViewModel).ToList()), + parentThrottled + .Where(p => p is null) + .Select(_ => (IReadOnlyList?)null) + ) + .ObserveOn(_rxSchedulerService.GetWorkerScheduler()) + .SubscribeOn(_rxSchedulerService.GetUIScheduler()) + .Publish(null) + .RefCount(); tab.CurrentLocation.Subscribe((_) => _markedItems.OnNext(Enumerable.Empty())); @@ -83,7 +124,7 @@ namespace FileTime.App.Core.ViewModels return await items .ToAsyncEnumerable() - .SelectAwait(async i => await i.ResolveAsync(true)) + .SelectAwait(async i => await i.ResolveAsync(forceResolve: true, itemInitializationSettings: new ItemInitializationSettings(true))) .ToListAsync(); } } diff --git a/src/Core/FileTime.Core.Abstraction/Models/IAbsolutePath.cs b/src/Core/FileTime.Core.Abstraction/Models/IAbsolutePath.cs index 23b501f..f0f8ea5 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/IAbsolutePath.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/IAbsolutePath.cs @@ -10,7 +10,7 @@ namespace FileTime.Core.Models FullName Path { get; } AbsolutePathType Type { get; } - Task ResolveAsync(bool forceResolve = false); - Task ResolveAsyncSafe(bool forceResolve = false); + Task ResolveAsync(bool forceResolve = false, ItemInitializationSettings itemInitializationSettings = default); + Task ResolveAsyncSafe(bool forceResolve = false, ItemInitializationSettings itemInitializationSettings = default); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Models/ItemInitializationSettings.cs b/src/Core/FileTime.Core.Abstraction/Models/ItemInitializationSettings.cs new file mode 100644 index 0000000..82a14eb --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/Models/ItemInitializationSettings.cs @@ -0,0 +1,12 @@ +namespace FileTime.Core.Models +{ + public readonly struct ItemInitializationSettings + { + public readonly bool SkipChildInitialization; + + public ItemInitializationSettings(bool skipChildInitialization) + { + SkipChildInitialization = skipChildInitialization; + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Services/IContentProvider.cs b/src/Core/FileTime.Core.Abstraction/Services/IContentProvider.cs index 15e83e7..82ab077 100644 --- a/src/Core/FileTime.Core.Abstraction/Services/IContentProvider.cs +++ b/src/Core/FileTime.Core.Abstraction/Services/IContentProvider.cs @@ -6,8 +6,18 @@ namespace FileTime.Core.Services { public interface IContentProvider : IContainer, IOnContainerEnter { - Task GetItemByFullNameAsync(FullName fullName, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown); - Task GetItemByNativePathAsync(NativePath nativePath, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown); + Task GetItemByFullNameAsync( + FullName fullName, + bool forceResolve = false, + AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, + ItemInitializationSettings itemInitializationSettings = default); + + Task GetItemByNativePathAsync( + NativePath nativePath, + bool forceResolve = false, + AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, + ItemInitializationSettings itemInitializationSettings = default); + Task> GetItemsByContainerAsync(FullName fullName); } } \ 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 f5d42e7..f1d4dd6 100644 --- a/src/Core/FileTime.Core.Models/AbsolutePath.cs +++ b/src/Core/FileTime.Core.Models/AbsolutePath.cs @@ -19,17 +19,17 @@ namespace FileTime.Core.Models Type = type; } - public async Task ResolveAsync(bool forceResolve = false) + public async Task ResolveAsync(bool forceResolve = false, ItemInitializationSettings itemInitializationSettings = default) { var provider = VirtualContentProvider ?? ContentProvider; - return await provider.GetItemByFullNameAsync(Path, forceResolve, Type); + return await provider.GetItemByFullNameAsync(Path, forceResolve, Type, itemInitializationSettings); } - public async Task ResolveAsyncSafe(bool forceResolve = false) + public async Task ResolveAsyncSafe(bool forceResolve = false, ItemInitializationSettings itemInitializationSettings = default) { try { - return await ResolveAsync(forceResolve); + return await ResolveAsync(forceResolve, itemInitializationSettings); } catch { return null; } } diff --git a/src/Core/FileTime.Core.Services/ContentProviderBase.cs b/src/Core/FileTime.Core.Services/ContentProviderBase.cs index 69ec570..2eb8745 100644 --- a/src/Core/FileTime.Core.Services/ContentProviderBase.cs +++ b/src/Core/FileTime.Core.Services/ContentProviderBase.cs @@ -49,9 +49,17 @@ namespace FileTime.Core.Services } public virtual Task OnEnter() => Task.CompletedTask; - public virtual async Task GetItemByFullNameAsync(FullName fullName, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown) - => await GetItemByNativePathAsync(GetNativePath(fullName), forceResolve, forceResolvePathType); - public abstract Task GetItemByNativePathAsync(NativePath nativePath, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown); + public virtual async Task GetItemByFullNameAsync( + FullName fullName, + bool forceResolve = false, + AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, + ItemInitializationSettings itemInitializationSettings = default) + => await GetItemByNativePathAsync(GetNativePath(fullName), forceResolve, forceResolvePathType, itemInitializationSettings); + public abstract Task GetItemByNativePathAsync( + NativePath nativePath, + bool forceResolve = false, + AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, + ItemInitializationSettings itemInitializationSettings = default); public abstract Task> GetItemsByContainerAsync(FullName fullName); public abstract NativePath GetNativePath(FullName fullName); } diff --git a/src/Core/FileTime.Core.Services/Tab.cs b/src/Core/FileTime.Core.Services/Tab.cs index 3d9baf8..fb1caa0 100644 --- a/src/Core/FileTime.Core.Services/Tab.cs +++ b/src/Core/FileTime.Core.Services/Tab.cs @@ -16,7 +16,7 @@ namespace FileTime.Core.Services public Tab() { - CurrentLocation = _currentLocation.DistinctUntilChanged().Do(_ => {; }).Publish(null).RefCount(); + CurrentLocation = _currentLocation.DistinctUntilChanged().Publish(null).RefCount(); CurrentItems = Observable.Merge( CurrentLocation @@ -69,7 +69,7 @@ namespace FileTime.Core.Services private IObservable GetSelectedItemByLocation(IContainer? currentLocation) { //TODO: - return currentLocation?.Items?.Select(i => i.FirstOrDefault()) ?? Observable.Return((IAbsolutePath?)null); + return currentLocation?.Items?.Select(i => i?.FirstOrDefault()) ?? Observable.Return((IAbsolutePath?)null); } public void SetCurrentLocation(IContainer newLocation) => _currentLocation.OnNext(newLocation); diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs index deae7c3..c55da1f 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs @@ -26,6 +26,7 @@ namespace FileTime.GuiApp .AddSingleton(s => s.GetRequiredService()) .AddSingleton() .AddSingleton() + .AddSingleton() //TODO: move?? .AddTransient() .AddTransient() diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/AvaloniaRxSchedulerService.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/AvaloniaRxSchedulerService.cs new file mode 100644 index 0000000..1f8032c --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/AvaloniaRxSchedulerService.cs @@ -0,0 +1,13 @@ +using System.Reactive.Concurrency; +using FileTime.App.Core.Services; +using ReactiveUI; + +namespace FileTime.GuiApp.Services +{ + public class AvaloniaRxSchedulerService : IRxSchedulerService + { + public IScheduler GetUIScheduler() => RxApp.MainThreadScheduler; + + public IScheduler GetWorkerScheduler() => RxApp.TaskpoolScheduler; + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml index 882d542..175b718 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml @@ -103,6 +103,23 @@ + + + + + + + - diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.DirectoryHelper.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.DirectoryHelper.cs index 2155e09..9ffc9e4 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.DirectoryHelper.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.DirectoryHelper.cs @@ -19,17 +19,5 @@ 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 5ca5764..6d7d973 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -1,4 +1,5 @@ using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Runtime.InteropServices; using FileTime.Core.Enums; using FileTime.Core.Models; @@ -31,9 +32,14 @@ namespace FileTime.Providers.Local Items.OnNext(rootDirectories.Select(DirectoryToAbsolutePath).ToList()); } - public override Task GetItemByNativePathAsync(NativePath nativePath, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown) + public override Task GetItemByNativePathAsync( + NativePath nativePath, + bool forceResolve = false, + AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, + ItemInitializationSettings itemInitializationSettings = default) { var path = nativePath.Path; + Exception? innerException; try { if ((path?.Length ?? 0) == 0) @@ -42,7 +48,7 @@ namespace FileTime.Providers.Local } else if (Directory.Exists(path)) { - return Task.FromResult((IItem)DirectoryToContainer(new DirectoryInfo(path!.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar))); + return Task.FromResult((IItem)DirectoryToContainer(new DirectoryInfo(path!.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar), !itemInitializationSettings.SkipChildInitialization)); } else if (File.Exists(path)) { @@ -56,20 +62,24 @@ namespace FileTime.Providers.Local _ => "Directory or file" }; - if (forceResolvePathType == AbsolutePathType.Container) throw new DirectoryNotFoundException($"{type} not found: '{path}'"); - throw new FileNotFoundException(type + " not found", path); + innerException = forceResolvePathType switch + { + AbsolutePathType.Container => new DirectoryNotFoundException($"{type} not found: '{path}'"), + _ => new FileNotFoundException(type + " not found", path) + }; } catch (Exception e) { if (!forceResolve) throw new Exception($"Could not resolve path '{nativePath.Path}' and {nameof(forceResolve)} is false.", e); - - return forceResolvePathType switch - { - AbsolutePathType.Container => Task.FromResult((IItem)CreateEmptyContainer(nativePath, Observable.Return(new List() { e }))), - AbsolutePathType.Element => Task.FromResult(CreateEmptyElement(nativePath)), - _ => throw new Exception($"Could not resolve path '{nativePath.Path}' and could not force create, because {nameof(forceResolvePathType)} is {nameof(AbsolutePathType.Unknown)}.", e) - }; + innerException = e; } + + return forceResolvePathType switch + { + AbsolutePathType.Container => Task.FromResult((IItem)CreateEmptyContainer(nativePath, Observable.Return(new List() { innerException }))), + AbsolutePathType.Element => Task.FromResult(CreateEmptyElement(nativePath)), + _ => throw new Exception($"Could not resolve path '{nativePath.Path}' and could not force create, because {nameof(forceResolvePathType)} is {nameof(AbsolutePathType.Unknown)}.", innerException) + }; } private Container CreateEmptyContainer(NativePath nativePath, IObservable>? exceptions = null) @@ -108,8 +118,8 @@ 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(GetFilesSafe(directoryInfo).Select(FileToAbsolutePath)).ToList(); + private List GetItemsByContainer(FullName fullName) => GetItemsByContainer(new DirectoryInfo(GetNativePath(fullName).Path)); + private List GetItemsByContainer(DirectoryInfo directoryInfo) => directoryInfo.GetDirectories().Select(DirectoryToAbsolutePath).Concat(directoryInfo.GetFiles().Select(FileToAbsolutePath)).ToList(); private IAbsolutePath DirectoryToAbsolutePath(DirectoryInfo directoryInfo) { @@ -123,7 +133,7 @@ namespace FileTime.Providers.Local return new AbsolutePath(this, fullName, AbsolutePathType.Element); } - private Container DirectoryToContainer(DirectoryInfo directoryInfo) + private Container DirectoryToContainer(DirectoryInfo directoryInfo, bool initializeChildren = true) { var fullName = GetFullName(directoryInfo.FullName); var parentFullName = fullName.GetParent(); @@ -131,6 +141,7 @@ namespace FileTime.Providers.Local this, parentFullName ?? new FullName(""), AbsolutePathType.Container); + var exceptions = new BehaviorSubject>(Enumerable.Empty()); return new( directoryInfo.Name, @@ -145,9 +156,24 @@ namespace FileTime.Providers.Local true, GetDirectoryAttributes(directoryInfo), this, - Observable.Return(Enumerable.Empty()), - Observable.Return(GetItemsByContainer(directoryInfo)) + exceptions, + Observable.FromAsync(async () => await Task.Run(InitChildren)) ); + + Task?> InitChildren() + { + List? result = null; + try + { + result = initializeChildren ? (List?)GetItemsByContainer(directoryInfo) : null; + } + catch (Exception e) + { + exceptions.OnNext(new List() { e }); + } + + return Task.FromResult(result); + } } private Element FileToElement(FileInfo fileInfo)