diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Models/BindedCollection.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Models/BindedCollection.cs new file mode 100644 index 0000000..fb2a153 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Models/BindedCollection.cs @@ -0,0 +1,26 @@ +using System.Collections.ObjectModel; +using DynamicData; + +namespace FileTime.App.Core.Models +{ + public class BindedCollection : IDisposable + { + private readonly IDisposable _disposable; + public ReadOnlyObservableCollection Collection { get; } + public BindedCollection(IObservable> dynamicList) + { + _disposable = dynamicList + .Bind(out var collection) + .DisposeMany() + .Subscribe(); + + Collection = collection; + } + + public void Dispose() + { + _disposable.Dispose(); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerSizeContainerViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerSizeContainerViewModel.cs index 0be724c..5d01f51 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerSizeContainerViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerSizeContainerViewModel.cs @@ -3,7 +3,7 @@ using InitableService; namespace FileTime.App.Core.ViewModels { - public interface IContainerSizeContainerViewModel : IItemViewModel, IInitable + public interface IContainerSizeContainerViewModel : IItemViewModel, IInitable { long Size { get; set; } } diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerViewModel.cs index 3669bfe..2125333 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerViewModel.cs @@ -3,7 +3,7 @@ using InitableService; namespace FileTime.App.Core.ViewModels { - public interface IContainerViewModel : IItemViewModel, IInitable + public interface IContainerViewModel : IItemViewModel, IInitable { IContainer? Container { get; } } diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IElementViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IElementViewModel.cs index 195345c..84fef93 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IElementViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IElementViewModel.cs @@ -3,7 +3,7 @@ using InitableService; namespace FileTime.App.Core.ViewModels { - public interface IElementViewModel : IItemViewModel, IInitable + public interface IElementViewModel : IItemViewModel, IInitable { long? Size { get; set; } } diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IFileViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IFileViewModel.cs index 1007202..22118e6 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IFileViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IFileViewModel.cs @@ -3,7 +3,7 @@ using InitableService; namespace FileTime.App.Core.ViewModels { - public interface IFileViewModel : IElementViewModel, IInitable + public interface IFileViewModel : IElementViewModel, IInitable { } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IItemViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IItemViewModel.cs index e9b9d30..ceeded9 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IItemViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IItemViewModel.cs @@ -1,4 +1,3 @@ -using System.Reactive.Subjects; using FileTime.App.Core.Models; using FileTime.App.Core.Models.Enums; using FileTime.Core.Models; @@ -6,16 +5,17 @@ using InitableService; namespace FileTime.App.Core.ViewModels { - public interface IItemViewModel : IInitable + public interface IItemViewModel : IInitable { IItem? BaseItem { get; set; } IObservable>? DisplayName { get; set; } string? DisplayNameText { get; set; } IObservable? IsSelected { get; set; } IObservable? IsMarked { get; set; } - BehaviorSubject IsAlternative { get; } + IObservable IsAlternative { get; } IObservable ViewMode { get; set; } DateTime? CreatedAt { get; set; } string? Attributes { get; set; } + bool EqualsTo(IItemViewModel? itemViewModel); } } \ 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 ef21606..7eb3a41 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs @@ -1,4 +1,6 @@ +using DynamicData; +using FileTime.App.Core.Models; using FileTime.Core.Models; using FileTime.Core.Services; using InitableService; @@ -12,9 +14,13 @@ namespace FileTime.App.Core.ViewModels IObservable IsSelected { get; } IObservable CurrentLocation { get; } IObservable CurrentSelectedItem { get; } - IObservable> CurrentItems { get; } + IObservable>?> CurrentItems { get; } IObservable> MarkedItems { get; } - IObservable?> SelectedsChildren { get; } - IObservable?> ParentsChildren { get; } + IObservable>?> SelectedsChildren { get; } + IObservable>?> ParentsChildren { get; } + BindedCollection? CurrentItemsCollection { get; } + BindedCollection? SelectedsChildrenCollection { get; } + BindedCollection? ParentsChildrenCollection { get; } + IObservable?> CurrentItemsCollectionObservable { get; } } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Extensions/ViewModelExtensions.cs b/src/AppCommon/FileTime.App.Core/Extensions/ViewModelExtensions.cs index faf432f..377afae 100644 --- a/src/AppCommon/FileTime.App.Core/Extensions/ViewModelExtensions.cs +++ b/src/AppCommon/FileTime.App.Core/Extensions/ViewModelExtensions.cs @@ -8,7 +8,7 @@ namespace FileTime.App.Core.Extensions public static IAbsolutePath ToAbsolutePath(this IItemViewModel itemViewModel) { var item = itemViewModel.BaseItem ?? throw new ArgumentException($"{nameof(itemViewModel)} does not have {nameof(IItemViewModel.BaseItem)}"); - return new AbsolutePath(item.Provider, item.FullName ?? throw new ArgumentException($"Parameter does not have {nameof(IItem.FullName)}"), item.Type); + return new AbsolutePath(item); } } } \ 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 e2fd5b3..723e46d 100644 --- a/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj +++ b/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj @@ -16,6 +16,7 @@ + diff --git a/src/AppCommon/FileTime.App.Core/Services/CommandHandler/NavigationCommandHandler.cs b/src/AppCommon/FileTime.App.Core/Services/CommandHandler/NavigationCommandHandler.cs index 2c38e0f..650e8a8 100644 --- a/src/AppCommon/FileTime.App.Core/Services/CommandHandler/NavigationCommandHandler.cs +++ b/src/AppCommon/FileTime.App.Core/Services/CommandHandler/NavigationCommandHandler.cs @@ -12,7 +12,7 @@ namespace FileTime.App.Core.Services.CommandHandler private ITabViewModel? _selectedTab; private IContainer? _currentLocation; private IItemViewModel? _currentSelectedItem; - private List _currentItems = new(); + private IEnumerable _currentItems = Enumerable.Empty(); public NavigationCommandHandler(IAppState appState) { @@ -21,7 +21,7 @@ namespace FileTime.App.Core.Services.CommandHandler _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); - _appState.SelectedTab.Select(t => t == null ? Observable.Return(Enumerable.Empty()) : t.CurrentItems).Switch().Subscribe(i => _currentItems = i.ToList()); + _appState.SelectedTab.Select(t => t?.CurrentItemsCollectionObservable ?? Observable.Return((IEnumerable?)Enumerable.Empty())).Switch().Subscribe(i => _currentItems = i ?? Enumerable.Empty()); AddCommandHandlers(new (Commands, Func)[] { @@ -48,13 +48,13 @@ namespace FileTime.App.Core.Services.CommandHandler private Task MoveCursorDown() { - SelectNewSelectedItem(i => i.SkipWhile(i => i != _currentSelectedItem).Skip(1).FirstOrDefault()); + SelectNewSelectedItem(i => i.SkipWhile(i => i.EqualsTo(_currentSelectedItem)).Skip(1).FirstOrDefault()); return Task.CompletedTask; } private Task MoveCursorUp() { - SelectNewSelectedItem(i => i.TakeWhile(i => i != _currentSelectedItem).LastOrDefault()); + SelectNewSelectedItem(i => i.TakeWhile(i => i.EqualsTo(_currentSelectedItem)).LastOrDefault()); return Task.CompletedTask; } diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs index 34ab930..18fd13c 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs @@ -2,7 +2,6 @@ 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; diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ContainerSizeContainerViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerSizeContainerViewModel.cs index cf5b60d..856c0e5 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ContainerSizeContainerViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerSizeContainerViewModel.cs @@ -14,9 +14,9 @@ namespace FileTime.App.Core.ViewModels { } - public void Init(IContainer item, ITabViewModel parentTab, int index) + public void Init(IContainer item, ITabViewModel parentTab) { - Init((IItem)item, parentTab, index); + Init((IItem)item, parentTab); } } } \ 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 bd492a7..ab8fe8c 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs @@ -13,9 +13,9 @@ namespace FileTime.App.Core.ViewModels { } - public void Init(IContainer item, ITabViewModel parentTab, int index) + public void Init(IContainer item, ITabViewModel parentTab) { - Init((IItem)item, parentTab, index); + Init((IItem)item, parentTab); } } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs index 4482328..e2944ad 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs @@ -14,9 +14,9 @@ namespace FileTime.App.Core.ViewModels { } - public void Init(IElement item, ITabViewModel parentTab, int index) + public void Init(IElement item, ITabViewModel parentTab) { - Init((IItem)item, parentTab, index); + Init((IItem)item, parentTab); } } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/FileViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/FileViewModel.cs index b6adff2..7dd1aa6 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/FileViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/FileViewModel.cs @@ -11,9 +11,9 @@ namespace FileTime.App.Core.ViewModels { } - public void Init(IFileElement item, ITabViewModel parentTab, int index) + public void Init(IFileElement item, ITabViewModel parentTab) { - Init((IElement)item, parentTab, index); + Init((IElement)item, parentTab); } } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs index d9251cc..7ae6409 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs @@ -1,9 +1,9 @@ using System.Reactive.Linq; -using System.Reactive.Subjects; using FileTime.App.Core.Models; using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Services; using FileTime.Core.Models; +using MoreLinq; using MvvmGen; namespace FileTime.App.Core.ViewModels @@ -38,16 +38,16 @@ namespace FileTime.App.Core.ViewModels private string? _attributes; [Property] - private BehaviorSubject _isAlternative = new(false); + private IObservable _isAlternative; - public void Init(IItem item, ITabViewModel parentTab, int index) + public void Init(IItem item, ITabViewModel parentTab) { BaseItem = item; DisplayName = _appState.SearchText.Select(s => _itemNameConverterService.GetDisplayName(item.DisplayName, s)); DisplayNameText = item.DisplayName; IsMarked = parentTab.MarkedItems.Select(m => m.Contains(item.FullName)); - IsSelected = parentTab.CurrentSelectedItem.Select(i => i == this); - IsAlternative.OnNext(index % 2 == 0); + IsSelected = parentTab.CurrentSelectedItem.Select(EqualsTo); + IsAlternative = parentTab.CurrentItemsCollectionObservable.Select(c => c?.Index().FirstOrDefault(i => EqualsTo(i.Value)).Key % 2 == 0); ViewMode = Observable.CombineLatest(IsMarked, IsSelected, IsAlternative, GenerateViewMode); Attributes = item.Attributes; CreatedAt = item.CreatedAt; @@ -63,5 +63,10 @@ namespace FileTime.App.Core.ViewModels (true, false, false) => ItemViewMode.Marked, _ => ItemViewMode.Default }; + + public bool EqualsTo(IItemViewModel? itemViewModel) + { + return BaseItem?.FullName?.Path is string path && path == itemViewModel?.BaseItem?.FullName?.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 9e4e2df..e30c5c7 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs @@ -1,15 +1,19 @@ -using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Reactive.Subjects; +using DynamicData; using FileTime.App.Core.Extensions; +using FileTime.App.Core.Models; using FileTime.App.Core.Services; using FileTime.Core.Models; using FileTime.Core.Services; +using FileTime.Tools.Extensions; using Microsoft.Extensions.DependencyInjection; +using MvvmGen; namespace FileTime.App.Core.ViewModels { - public class TabViewModel : ITabViewModel, IDisposable + [ViewModel] + public partial class TabViewModel : ITabViewModel, IDisposable { private readonly IServiceProvider _serviceProvider; private readonly IItemNameConverterService _itemNameConverterService; @@ -26,10 +30,22 @@ namespace FileTime.App.Core.ViewModels public IObservable CurrentLocation { get; private set; } = null!; public IObservable CurrentSelectedItem { get; private set; } = null!; - public IObservable> CurrentItems { get; private set; } = null!; + 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 IObservable>?> SelectedsChildren { get; private set; } = null!; + public IObservable>?> ParentsChildren { get; private set; } = null!; + + public IObservable?> CurrentItemsCollectionObservable { get; private set; } = null!; + + [Property] + private BindedCollection? _currentItemsCollection; + + [Property] + private BindedCollection? _parentsChildrenCollection; + + [Property] + private BindedCollection? _selectedsChildrenCollection; + public TabViewModel( IServiceProvider serviceProvider, @@ -53,100 +69,125 @@ namespace FileTime.App.Core.ViewModels CurrentLocation = tab.CurrentLocation.AsObservable(); CurrentItems = tab.CurrentItems - .Select(items => - items == null - ? new List() - : items.Select(MapItemToViewModel).ToList()) + .Select(items => items?.Transform(MapItemToViewModel)) .ObserveOn(_rxSchedulerService.GetWorkerScheduler()) .SubscribeOn(_rxSchedulerService.GetUIScheduler()) - .Publish(new List()) + .Publish(null) .RefCount(); CurrentSelectedItem = Observable.CombineLatest( CurrentItems, tab.CurrentSelectedItem, - (currentItems, currentSelectedItemPath) => currentItems.FirstOrDefault(i => i.BaseItem?.FullName == currentSelectedItemPath?.Path) + (currentItems, currentSelectedItemPath) => + currentItems == null + ? Observable.Return((IItemViewModel?)null) + : currentItems + .ToCollection() + .Select(items => items.FirstOrDefault(i => i.BaseItem?.FullName == currentSelectedItemPath?.Path)) ) + .Switch() .Publish(null) .RefCount(); - var currentSelectedItemThrottled = CurrentSelectedItem.Throttle(TimeSpan.FromMilliseconds(250)).Publish(null).RefCount(); - SelectedsChildren = Observable.Merge( - currentSelectedItemThrottled - .WhereNotNull() - .OfType() - .Where(c => c?.Container is not null) - .Select(c => c.Container!.Items) - .Switch() - .Select(items => Observable.FromAsync(async () => await Map(items))) - .Switch() - .Select(items => items?.Select(MapItemToViewModel).ToList()), - currentSelectedItemThrottled - .Where(c => c is null || c is not IContainerViewModel) - .Select(_ => (IReadOnlyList?)null) - ) - .ObserveOn(_rxSchedulerService.GetWorkerScheduler()) - .SubscribeOn(_rxSchedulerService.GetUIScheduler()) - .Publish(null) - .RefCount(); + SelectedsChildren = InitSelectedsChildren(); + ParentsChildren = InitParentsChildren(); - var parentThrottled = CurrentLocation - .Select(l => l?.Parent) - .DistinctUntilChanged() + CurrentItemsCollectionObservable = CurrentItems + .Select(c => c != null ? c.ToCollection() : Observable.Return((IReadOnlyCollection?)null)) + .Switch() .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(); + CurrentItems.Subscribe(children => + { + CurrentItemsCollection?.Dispose(); + CurrentItemsCollection = children.MapNull(c => new BindedCollection(c!)); + }); + + ParentsChildren.Subscribe(children => + { + ParentsChildrenCollection?.Dispose(); + ParentsChildrenCollection = children.MapNull(c => new BindedCollection(c!)); + }); + + SelectedsChildren.Subscribe(children => + { + SelectedsChildrenCollection?.Dispose(); + SelectedsChildrenCollection = children.MapNull(c => new BindedCollection(c!)); + }); tab.CurrentLocation.Subscribe((_) => _markedItems.OnNext(Enumerable.Empty())); - static async Task?> Map(IEnumerable? items) + IObservable>?> InitSelectedsChildren() { - if (items == null) return null; + var currentSelectedItemThrottled = CurrentSelectedItem.Throttle(TimeSpan.FromMilliseconds(250)).Publish(null).RefCount(); + return Observable.Merge( + currentSelectedItemThrottled + .WhereNotNull() + .OfType() + .Where(c => c?.Container is not null) + .Select(c => c.Container!.Items) + .Switch() + .Select(i => i?.TransformAsync(MapItem).Transform(MapItemToViewModel)), + currentSelectedItemThrottled + .Where(c => c is null || c is not IContainerViewModel) + .Select(_ => (IObservable>?)null) + ) + .ObserveOn(_rxSchedulerService.GetWorkerScheduler()) + .SubscribeOn(_rxSchedulerService.GetUIScheduler()) + .Publish(null) + .RefCount(); + } - return await items - .ToAsyncEnumerable() - .SelectAwait(async i => await i.ResolveAsync(forceResolve: true, itemInitializationSettings: new ItemInitializationSettings(true))) - .ToListAsync(); + IObservable>?> InitParentsChildren() + { + var parentThrottled = CurrentLocation + .Select(l => l?.Parent) + .DistinctUntilChanged() + .Publish(null) + .RefCount(); + + return 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 => items?.TransformAsync(MapItem).Transform(MapItemToViewModel)), + parentThrottled + .Where(p => p is null) + .Select(_ => (IObservable>?)null) + ) + .ObserveOn(_rxSchedulerService.GetWorkerScheduler()) + .SubscribeOn(_rxSchedulerService.GetUIScheduler()) + .Publish(null) + .RefCount(); } } - private IItemViewModel MapItemToViewModel(IItem item, int index) + private static async Task MapItem(IAbsolutePath item) + => await item.ResolveAsync(forceResolve: true, itemInitializationSettings: new ItemInitializationSettings(true)); + + private IItemViewModel MapItemToViewModel(IItem item) { if (item is IContainer container) { - var containerViewModel = _serviceProvider.GetInitableResolver(container, this, index).GetRequiredService(); + var containerViewModel = _serviceProvider.GetInitableResolver(container, this).GetRequiredService(); return containerViewModel; } else if (item is IFileElement fileElement) { - var fileViewModel = _serviceProvider.GetInitableResolver(fileElement, this, index).GetRequiredService(); + var fileViewModel = _serviceProvider.GetInitableResolver(fileElement, this).GetRequiredService(); fileViewModel.Size = fileElement.Size; return fileViewModel; } else if (item is IElement element) { - var elementViewModel = _serviceProvider.GetInitableResolver(element, this, index).GetRequiredService(); + var elementViewModel = _serviceProvider.GetInitableResolver(element, this).GetRequiredService(); return elementViewModel; } diff --git a/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj b/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj index 3dc96ef..0bf3e28 100644 --- a/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj +++ b/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Core/FileTime.Core.Abstraction/Models/IContainer.cs b/src/Core/FileTime.Core.Abstraction/Models/IContainer.cs index 2f87b23..b008d5c 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/IContainer.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/IContainer.cs @@ -1,8 +1,10 @@ +using DynamicData; + namespace FileTime.Core.Models { public interface IContainer : IItem { - IObservable?> Items { get; } + IObservable>?> Items { get; } IObservable IsLoading { get; } } } \ 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 be77e4e..54539cb 100644 --- a/src/Core/FileTime.Core.Abstraction/Services/ITab.cs +++ b/src/Core/FileTime.Core.Abstraction/Services/ITab.cs @@ -1,3 +1,4 @@ +using DynamicData; using FileTime.Core.Models; using InitableService; @@ -7,7 +8,7 @@ namespace FileTime.Core.Services { IObservable CurrentLocation { get; } IObservable CurrentSelectedItem { get; } - IObservable?> CurrentItems { get; } + IObservable>?> CurrentItems { get; } void SetCurrentLocation(IContainer newLocation); void AddSelectedItemsTransformator(ItemsTransformator transformator); diff --git a/src/Core/FileTime.Core.Models/AbsolutePath.cs b/src/Core/FileTime.Core.Models/AbsolutePath.cs index f1d4dd6..c57cbf1 100644 --- a/src/Core/FileTime.Core.Models/AbsolutePath.cs +++ b/src/Core/FileTime.Core.Models/AbsolutePath.cs @@ -19,6 +19,14 @@ namespace FileTime.Core.Models Type = type; } + public AbsolutePath(IItem item, IContentProvider? virtualContentProvider = null) + { + ContentProvider = item.Provider; + Path = item.FullName ?? throw new ArgumentException($"{nameof(item.FullName)} can not be null.", nameof(item)); + VirtualContentProvider = virtualContentProvider; + Type = item.Type; + } + public async Task ResolveAsync(bool forceResolve = false, ItemInitializationSettings itemInitializationSettings = default) { var provider = VirtualContentProvider ?? ContentProvider; diff --git a/src/Core/FileTime.Core.Models/Container.cs b/src/Core/FileTime.Core.Models/Container.cs index 3221231..6837ab7 100644 --- a/src/Core/FileTime.Core.Models/Container.cs +++ b/src/Core/FileTime.Core.Models/Container.cs @@ -1,5 +1,6 @@ using System.Reactive.Linq; using System.Reactive.Subjects; +using DynamicData; using FileTime.Core.Enums; using FileTime.Core.Services; @@ -19,7 +20,7 @@ namespace FileTime.Core.Models string? Attributes, IContentProvider Provider, IObservable> Exceptions, - IObservable?> Items) : IContainer + IObservable>?> Items) : IContainer { BehaviorSubject IsLoading { get; } = new BehaviorSubject(false); IObservable IContainer.IsLoading => IsLoading.AsObservable(); diff --git a/src/Core/FileTime.Core.Services/ContentProviderBase.cs b/src/Core/FileTime.Core.Services/ContentProviderBase.cs index 2eb8745..e9b5b9d 100644 --- a/src/Core/FileTime.Core.Services/ContentProviderBase.cs +++ b/src/Core/FileTime.Core.Services/ContentProviderBase.cs @@ -1,5 +1,6 @@ using System.Reactive.Linq; using System.Reactive.Subjects; +using DynamicData; using FileTime.Core.Enums; using FileTime.Core.Models; @@ -7,9 +8,9 @@ namespace FileTime.Core.Services { public abstract class ContentProviderBase : IContentProvider { - protected BehaviorSubject> Items { get; } = new BehaviorSubject>(new List()); + protected BehaviorSubject>?> Items { get; } = new (null); - IObservable> IContainer.Items => Items; + IObservable>?> IContainer.Items => Items; public string Name { get; } diff --git a/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj b/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj index 9615311..f2cc8f8 100644 --- a/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj +++ b/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Core/FileTime.Core.Services/Tab.cs b/src/Core/FileTime.Core.Services/Tab.cs index fb1caa0..d6c632c 100644 --- a/src/Core/FileTime.Core.Services/Tab.cs +++ b/src/Core/FileTime.Core.Services/Tab.cs @@ -1,5 +1,6 @@ using System.Reactive.Linq; using System.Reactive.Subjects; +using DynamicData; using FileTime.Core.Models; namespace FileTime.Core.Services @@ -10,8 +11,9 @@ namespace FileTime.Core.Services private readonly BehaviorSubject _currentSelectedItem = new(null); private readonly List _transformators = new(); private IAbsolutePath? _currentSelectedItemCached; + public IObservable CurrentLocation { get; } - public IObservable?> CurrentItems { get; } + public IObservable>?> CurrentItems { get; } public IObservable CurrentSelectedItem { get; } public Tab() @@ -23,53 +25,54 @@ namespace FileTime.Core.Services .Where(c => c is not null) .Select(c => c!.Items) .Switch() - .Select(i => i == null ? Observable.Return?>(null) : Observable.FromAsync(async () => await MapItems(i))) - .Switch(), + .Select(items => items?.TransformAsync(MapItem)), CurrentLocation .Where(c => c is null) - .Select(_ => Enumerable.Empty()) + .Select(_ => (IObservable>?)null) ) - .Publish(Enumerable.Empty()) + .Publish((IObservable>?)null) .RefCount(); - CurrentSelectedItem = CurrentLocation - .Select(GetSelectedItemByLocation) - .Switch() - .Merge(_currentSelectedItem) + CurrentSelectedItem = + Observable.CombineLatest( + CurrentItems + .Select(c => + c == null + ? Observable.Return?>(null) + : c.ToCollection() + ) + .Switch(), + _currentSelectedItem, + (items, selected) => + { + if (selected != null && (items?.Any(i => i.FullName == selected.Path) ?? true)) return selected; + if (items == null || items.Count == 0) return null; + + return GetSelectedItemByItems(items); + } + ) .DistinctUntilChanged() .Publish(null) .RefCount(); - CurrentSelectedItem.Subscribe(s => _currentSelectedItemCached = s); + CurrentSelectedItem.Subscribe(s => + { + _currentSelectedItemCached = s; + _currentSelectedItem.OnNext(s); + }); } - private async Task> MapItems(IEnumerable items) - { - IEnumerable resolvedItems = await items - .ToAsyncEnumerable() - .SelectAwait(async i => await i.ResolveAsync(true)) - .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); - } + private async Task MapItem(IAbsolutePath item) => await item.ResolveAsync(true); public void Init(IContainer currentLocation) { _currentLocation.OnNext(currentLocation); } - private IObservable GetSelectedItemByLocation(IContainer? currentLocation) + private static IAbsolutePath? GetSelectedItemByItems(IEnumerable items) { //TODO: - return currentLocation?.Items?.Select(i => i?.FirstOrDefault()) ?? Observable.Return((IAbsolutePath?)null); + return new AbsolutePath(items.First()); } public void SetCurrentLocation(IContainer newLocation) => _currentLocation.OnNext(newLocation); diff --git a/src/FileTime.sln b/src/FileTime.sln index 36e1f00..62117f1 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -47,6 +47,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.Providers.Local.Ab 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 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{8C3CFEFE-78A5-4940-B388-D15FCE02ECE9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Tools", "Tools\FileTime.Tools\FileTime.Tools.csproj", "{B7A45654-E56C-43C8-998E-0F4661395540}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -113,6 +117,10 @@ Global {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 + {B7A45654-E56C-43C8-998E-0F4661395540}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7A45654-E56C-43C8-998E-0F4661395540}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7A45654-E56C-43C8-998E-0F4661395540}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7A45654-E56C-43C8-998E-0F4661395540}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -134,6 +142,7 @@ Global {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} + {B7A45654-E56C-43C8-998E-0F4661395540} = {8C3CFEFE-78A5-4940-B388-D15FCE02ECE9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml index 175b718..e22fb0c 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml @@ -108,7 +108,7 @@ AutoScrollToSelectedItem="True" Classes="ContentListView" IsTabStop="True" - Items="{Binding AppState.SelectedTab^.ParentsChildren^}" + Items="{Binding AppState.SelectedTab^.ParentsChildrenCollection.Collection}" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Visible"> @@ -146,7 +146,7 @@ AutoScrollToSelectedItem="True" Classes="ContentListView" IsTabStop="True" - Items="{Binding AppState.SelectedTab^.CurrentItems^}" + Items="{Binding AppState.SelectedTab^.CurrentItemsCollection.Collection}" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Visible"> @@ -166,7 +166,7 @@ x:CompileBindings="False" FontWeight="Bold" Foreground="{DynamicResource ErrorBrush}" - IsVisible="{Binding AppState.SelectedTab^.CurrentLocation^.Items^.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"> + IsVisible="{Binding AppState.SelectedTab^.CurrentItemsCollection.Collection.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"> Empty @@ -180,14 +180,14 @@ Fill="{DynamicResource ContentSeparatorBrush}" /> - + + IsVisible="{Binding AppState.SelectedTab^.SelectedsChildrenCollection.Collection.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}" + Items="{Binding AppState.SelectedTab^.SelectedsChildrenCollection.Collection}"> @@ -202,13 +202,13 @@ x:CompileBindings="False" FontWeight="Bold" Foreground="{DynamicResource ErrorBrush}" - IsVisible="{Binding AppState.SelectedTab^.SelectedsChildren^.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"> + IsVisible="{Binding AppState.SelectedTab^.SelectedsChildrenCollection.Collection.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"> Empty + diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs index 6d7d973..0c26c4a 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -1,19 +1,24 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Runtime.InteropServices; +using DynamicData; using FileTime.Core.Enums; using FileTime.Core.Models; using FileTime.Core.Services; namespace FileTime.Providers.Local { - public partial class LocalContentProvider : ContentProviderBase, ILocalContentProvider + public sealed partial class LocalContentProvider : ContentProviderBase, ILocalContentProvider { - protected bool IsCaseInsensitive { get; init; } + private readonly SourceList _rootDirectories = new(); + private readonly bool _isCaseInsensitive; public LocalContentProvider() : base("local") { - IsCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + _isCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + RefreshRootDirectories(); + + Items.OnNext(_rootDirectories.Connect()); } public override Task OnEnter() @@ -29,7 +34,11 @@ namespace FileTime.Providers.Local ? new DirectoryInfo("/").GetDirectories() : Environment.GetLogicalDrives().Select(d => new DirectoryInfo(d)); - Items.OnNext(rootDirectories.Select(DirectoryToAbsolutePath).ToList()); + _rootDirectories.Edit(actions => + { + actions.Clear(); + actions.AddRange(rootDirectories.Select(DirectoryToAbsolutePath)); + }); } public override Task GetItemByNativePathAsync( @@ -108,7 +117,7 @@ namespace FileTime.Providers.Local "???", this, nonNullExceptions, - Observable.Return?>(null) + Observable.Return>?>(null) ); } @@ -160,19 +169,24 @@ namespace FileTime.Providers.Local Observable.FromAsync(async () => await Task.Run(InitChildren)) ); - Task?> InitChildren() + Task>?> InitChildren() { - List? result = null; + SourceList? result = null; try { - result = initializeChildren ? (List?)GetItemsByContainer(directoryInfo) : null; + var items = initializeChildren ? (List?)GetItemsByContainer(directoryInfo) : null; + if (items != null) + { + result = new SourceList(); + result.AddRange(items); + } } catch (Exception e) { exceptions.OnNext(new List() { e }); } - return Task.FromResult(result); + return Task.FromResult(result?.Connect()); } } diff --git a/src/Tools/FileTime.Tools/Extensions/ObjectExtensions.cs b/src/Tools/FileTime.Tools/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..189e7e7 --- /dev/null +++ b/src/Tools/FileTime.Tools/Extensions/ObjectExtensions.cs @@ -0,0 +1,11 @@ +namespace FileTime.Tools.Extensions +{ + public static class ObjectExtensions + { + public static TResult? MapNull(this T obj, Func nullHandler, Func valueHandler) + => obj == null ? nullHandler() : valueHandler(obj); + + public static TResult? MapNull(this T obj, Func valueHandler) + => obj == null ? default : valueHandler(obj); + } +} \ No newline at end of file diff --git a/src/Tools/FileTime.Tools/FileTime.Tools.csproj b/src/Tools/FileTime.Tools/FileTime.Tools.csproj new file mode 100644 index 0000000..bafd05b --- /dev/null +++ b/src/Tools/FileTime.Tools/FileTime.Tools.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + +