Refactor: items with DynamicData

This commit is contained in:
2022-04-22 09:09:14 +02:00
parent da3ccf4317
commit 76c6e30154
31 changed files with 281 additions and 141 deletions

View File

@@ -0,0 +1,26 @@
using System.Collections.ObjectModel;
using DynamicData;
namespace FileTime.App.Core.Models
{
public class BindedCollection<T> : IDisposable
{
private readonly IDisposable _disposable;
public ReadOnlyObservableCollection<T> Collection { get; }
public BindedCollection(IObservable<IChangeSet<T>> dynamicList)
{
_disposable = dynamicList
.Bind(out var collection)
.DisposeMany()
.Subscribe();
Collection = collection;
}
public void Dispose()
{
_disposable.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -3,7 +3,7 @@ using InitableService;
namespace FileTime.App.Core.ViewModels namespace FileTime.App.Core.ViewModels
{ {
public interface IContainerSizeContainerViewModel : IItemViewModel, IInitable<IContainer, ITabViewModel, int> public interface IContainerSizeContainerViewModel : IItemViewModel, IInitable<IContainer, ITabViewModel>
{ {
long Size { get; set; } long Size { get; set; }
} }

View File

@@ -3,7 +3,7 @@ using InitableService;
namespace FileTime.App.Core.ViewModels namespace FileTime.App.Core.ViewModels
{ {
public interface IContainerViewModel : IItemViewModel, IInitable<IContainer, ITabViewModel, int> public interface IContainerViewModel : IItemViewModel, IInitable<IContainer, ITabViewModel>
{ {
IContainer? Container { get; } IContainer? Container { get; }
} }

View File

@@ -3,7 +3,7 @@ using InitableService;
namespace FileTime.App.Core.ViewModels namespace FileTime.App.Core.ViewModels
{ {
public interface IElementViewModel : IItemViewModel, IInitable<IElement, ITabViewModel, int> public interface IElementViewModel : IItemViewModel, IInitable<IElement, ITabViewModel>
{ {
long? Size { get; set; } long? Size { get; set; }
} }

View File

@@ -3,7 +3,7 @@ using InitableService;
namespace FileTime.App.Core.ViewModels namespace FileTime.App.Core.ViewModels
{ {
public interface IFileViewModel : IElementViewModel, IInitable<IFileElement, ITabViewModel, int> public interface IFileViewModel : IElementViewModel, IInitable<IFileElement, ITabViewModel>
{ {
} }
} }

View File

@@ -1,4 +1,3 @@
using System.Reactive.Subjects;
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Models.Enums;
using FileTime.Core.Models; using FileTime.Core.Models;
@@ -6,16 +5,17 @@ using InitableService;
namespace FileTime.App.Core.ViewModels namespace FileTime.App.Core.ViewModels
{ {
public interface IItemViewModel : IInitable<IItem, ITabViewModel, int> public interface IItemViewModel : IInitable<IItem, ITabViewModel>
{ {
IItem? BaseItem { get; set; } IItem? BaseItem { get; set; }
IObservable<IReadOnlyList<ItemNamePart>>? DisplayName { get; set; } IObservable<IReadOnlyList<ItemNamePart>>? DisplayName { get; set; }
string? DisplayNameText { get; set; } string? DisplayNameText { get; set; }
IObservable<bool>? IsSelected { get; set; } IObservable<bool>? IsSelected { get; set; }
IObservable<bool>? IsMarked { get; set; } IObservable<bool>? IsMarked { get; set; }
BehaviorSubject<bool> IsAlternative { get; } IObservable<bool> IsAlternative { get; }
IObservable<ItemViewMode> ViewMode { get; set; } IObservable<ItemViewMode> ViewMode { get; set; }
DateTime? CreatedAt { get; set; } DateTime? CreatedAt { get; set; }
string? Attributes { get; set; } string? Attributes { get; set; }
bool EqualsTo(IItemViewModel? itemViewModel);
} }
} }

View File

@@ -1,4 +1,6 @@
using DynamicData;
using FileTime.App.Core.Models;
using FileTime.Core.Models; using FileTime.Core.Models;
using FileTime.Core.Services; using FileTime.Core.Services;
using InitableService; using InitableService;
@@ -12,9 +14,13 @@ namespace FileTime.App.Core.ViewModels
IObservable<bool> IsSelected { get; } IObservable<bool> IsSelected { get; }
IObservable<IContainer?> CurrentLocation { get; } IObservable<IContainer?> CurrentLocation { get; }
IObservable<IItemViewModel?> CurrentSelectedItem { get; } IObservable<IItemViewModel?> CurrentSelectedItem { get; }
IObservable<IReadOnlyList<IItemViewModel>> CurrentItems { get; } IObservable<IObservable<IChangeSet<IItemViewModel>>?> CurrentItems { get; }
IObservable<IEnumerable<FullName>> MarkedItems { get; } IObservable<IEnumerable<FullName>> MarkedItems { get; }
IObservable<IReadOnlyList<IItemViewModel>?> SelectedsChildren { get; } IObservable<IObservable<IChangeSet<IItemViewModel>>?> SelectedsChildren { get; }
IObservable<IReadOnlyList<IItemViewModel>?> ParentsChildren { get; } IObservable<IObservable<IChangeSet<IItemViewModel>>?> ParentsChildren { get; }
BindedCollection<IItemViewModel>? CurrentItemsCollection { get; }
BindedCollection<IItemViewModel>? SelectedsChildrenCollection { get; }
BindedCollection<IItemViewModel>? ParentsChildrenCollection { get; }
IObservable<IReadOnlyCollection<IItemViewModel>?> CurrentItemsCollectionObservable { get; }
} }
} }

View File

@@ -8,7 +8,7 @@ namespace FileTime.App.Core.Extensions
public static IAbsolutePath ToAbsolutePath(this IItemViewModel itemViewModel) public static IAbsolutePath ToAbsolutePath(this IItemViewModel itemViewModel)
{ {
var item = itemViewModel.BaseItem ?? throw new ArgumentException($"{nameof(itemViewModel)} does not have {nameof(IItemViewModel.BaseItem)}"); 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);
} }
} }
} }

View File

@@ -16,6 +16,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" /> <ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
<ProjectReference Include="..\..\Core\FileTime.Core.Models\FileTime.Core.Models.csproj" /> <ProjectReference Include="..\..\Core\FileTime.Core.Models\FileTime.Core.Models.csproj" />
<ProjectReference Include="..\..\Tools\FileTime.Tools\FileTime.Tools.csproj" />
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" /> <ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -12,7 +12,7 @@ namespace FileTime.App.Core.Services.CommandHandler
private ITabViewModel? _selectedTab; private ITabViewModel? _selectedTab;
private IContainer? _currentLocation; private IContainer? _currentLocation;
private IItemViewModel? _currentSelectedItem; private IItemViewModel? _currentSelectedItem;
private List<IItemViewModel> _currentItems = new(); private IEnumerable<IItemViewModel> _currentItems = Enumerable.Empty<IItemViewModel>();
public NavigationCommandHandler(IAppState appState) public NavigationCommandHandler(IAppState appState)
{ {
@@ -21,7 +21,7 @@ namespace FileTime.App.Core.Services.CommandHandler
_appState.SelectedTab.Subscribe(t => _selectedTab = t); _appState.SelectedTab.Subscribe(t => _selectedTab = t);
_appState.SelectedTab.Select(t => t == null ? Observable.Return<IContainer?>(null) : t.CurrentLocation).Switch().Subscribe(l => _currentLocation = l); _appState.SelectedTab.Select(t => t == null ? Observable.Return<IContainer?>(null) : t.CurrentLocation).Switch().Subscribe(l => _currentLocation = l);
_appState.SelectedTab.Select(t => t == null ? Observable.Return<IItemViewModel?>(null) : t.CurrentSelectedItem).Switch().Subscribe(l => _currentSelectedItem = l); _appState.SelectedTab.Select(t => t == null ? Observable.Return<IItemViewModel?>(null) : t.CurrentSelectedItem).Switch().Subscribe(l => _currentSelectedItem = l);
_appState.SelectedTab.Select(t => t == null ? Observable.Return(Enumerable.Empty<IItemViewModel>()) : t.CurrentItems).Switch().Subscribe(i => _currentItems = i.ToList()); _appState.SelectedTab.Select(t => t?.CurrentItemsCollectionObservable ?? Observable.Return((IEnumerable<IItemViewModel>?)Enumerable.Empty<IItemViewModel>())).Switch().Subscribe(i => _currentItems = i ?? Enumerable.Empty<IItemViewModel>());
AddCommandHandlers(new (Commands, Func<Task>)[] AddCommandHandlers(new (Commands, Func<Task>)[]
{ {
@@ -48,13 +48,13 @@ namespace FileTime.App.Core.Services.CommandHandler
private Task MoveCursorDown() 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; return Task.CompletedTask;
} }
private Task MoveCursorUp() private Task MoveCursorUp()
{ {
SelectNewSelectedItem(i => i.TakeWhile(i => i != _currentSelectedItem).LastOrDefault()); SelectNewSelectedItem(i => i.TakeWhile(i => i.EqualsTo(_currentSelectedItem)).LastOrDefault());
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -2,7 +2,6 @@ using System.Collections.ObjectModel;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.ViewModels;
using MvvmGen; using MvvmGen;
using MoreLinq; using MoreLinq;

View File

@@ -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);
} }
} }
} }

View File

@@ -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);
} }
} }
} }

View File

@@ -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);
} }
} }
} }

View File

@@ -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);
} }
} }
} }

View File

@@ -1,9 +1,9 @@
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects;
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.Core.Models; using FileTime.Core.Models;
using MoreLinq;
using MvvmGen; using MvvmGen;
namespace FileTime.App.Core.ViewModels namespace FileTime.App.Core.ViewModels
@@ -38,16 +38,16 @@ namespace FileTime.App.Core.ViewModels
private string? _attributes; private string? _attributes;
[Property] [Property]
private BehaviorSubject<bool> _isAlternative = new(false); private IObservable<bool> _isAlternative;
public void Init(IItem item, ITabViewModel parentTab, int index) public void Init(IItem item, ITabViewModel parentTab)
{ {
BaseItem = item; BaseItem = item;
DisplayName = _appState.SearchText.Select(s => _itemNameConverterService.GetDisplayName(item.DisplayName, s)); DisplayName = _appState.SearchText.Select(s => _itemNameConverterService.GetDisplayName(item.DisplayName, s));
DisplayNameText = item.DisplayName; DisplayNameText = item.DisplayName;
IsMarked = parentTab.MarkedItems.Select(m => m.Contains(item.FullName)); IsMarked = parentTab.MarkedItems.Select(m => m.Contains(item.FullName));
IsSelected = parentTab.CurrentSelectedItem.Select(i => i == this); IsSelected = parentTab.CurrentSelectedItem.Select(EqualsTo);
IsAlternative.OnNext(index % 2 == 0); IsAlternative = parentTab.CurrentItemsCollectionObservable.Select(c => c?.Index().FirstOrDefault(i => EqualsTo(i.Value)).Key % 2 == 0);
ViewMode = Observable.CombineLatest(IsMarked, IsSelected, IsAlternative, GenerateViewMode); ViewMode = Observable.CombineLatest(IsMarked, IsSelected, IsAlternative, GenerateViewMode);
Attributes = item.Attributes; Attributes = item.Attributes;
CreatedAt = item.CreatedAt; CreatedAt = item.CreatedAt;
@@ -63,5 +63,10 @@ namespace FileTime.App.Core.ViewModels
(true, false, false) => ItemViewMode.Marked, (true, false, false) => ItemViewMode.Marked,
_ => ItemViewMode.Default _ => ItemViewMode.Default
}; };
public bool EqualsTo(IItemViewModel? itemViewModel)
{
return BaseItem?.FullName?.Path is string path && path == itemViewModel?.BaseItem?.FullName?.Path;
}
} }
} }

View File

@@ -1,15 +1,19 @@
using System.Reactive.Concurrency;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using DynamicData;
using FileTime.App.Core.Extensions; using FileTime.App.Core.Extensions;
using FileTime.App.Core.Models;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.Core.Models; using FileTime.Core.Models;
using FileTime.Core.Services; using FileTime.Core.Services;
using FileTime.Tools.Extensions;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using MvvmGen;
namespace FileTime.App.Core.ViewModels namespace FileTime.App.Core.ViewModels
{ {
public class TabViewModel : ITabViewModel, IDisposable [ViewModel]
public partial class TabViewModel : ITabViewModel, IDisposable
{ {
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IItemNameConverterService _itemNameConverterService; private readonly IItemNameConverterService _itemNameConverterService;
@@ -26,10 +30,22 @@ namespace FileTime.App.Core.ViewModels
public IObservable<IContainer?> CurrentLocation { get; private set; } = null!; public IObservable<IContainer?> CurrentLocation { get; private set; } = null!;
public IObservable<IItemViewModel?> CurrentSelectedItem { get; private set; } = null!; public IObservable<IItemViewModel?> CurrentSelectedItem { get; private set; } = null!;
public IObservable<IReadOnlyList<IItemViewModel>> CurrentItems { get; private set; } = null!; public IObservable<IObservable<IChangeSet<IItemViewModel>>?> CurrentItems { get; private set; } = null!;
public IObservable<IEnumerable<FullName>> MarkedItems { get; } public IObservable<IEnumerable<FullName>> MarkedItems { get; }
public IObservable<IReadOnlyList<IItemViewModel>?> SelectedsChildren { get; private set; } = null!; public IObservable<IObservable<IChangeSet<IItemViewModel>>?> SelectedsChildren { get; private set; } = null!;
public IObservable<IReadOnlyList<IItemViewModel>?> ParentsChildren { get; private set; } = null!; public IObservable<IObservable<IChangeSet<IItemViewModel>>?> ParentsChildren { get; private set; } = null!;
public IObservable<IReadOnlyCollection<IItemViewModel>?> CurrentItemsCollectionObservable { get; private set; } = null!;
[Property]
private BindedCollection<IItemViewModel>? _currentItemsCollection;
[Property]
private BindedCollection<IItemViewModel>? _parentsChildrenCollection;
[Property]
private BindedCollection<IItemViewModel>? _selectedsChildrenCollection;
public TabViewModel( public TabViewModel(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
@@ -53,100 +69,125 @@ namespace FileTime.App.Core.ViewModels
CurrentLocation = tab.CurrentLocation.AsObservable(); CurrentLocation = tab.CurrentLocation.AsObservable();
CurrentItems = tab.CurrentItems CurrentItems = tab.CurrentItems
.Select(items => .Select(items => items?.Transform(MapItemToViewModel))
items == null
? new List<IItemViewModel>()
: items.Select(MapItemToViewModel).ToList())
.ObserveOn(_rxSchedulerService.GetWorkerScheduler()) .ObserveOn(_rxSchedulerService.GetWorkerScheduler())
.SubscribeOn(_rxSchedulerService.GetUIScheduler()) .SubscribeOn(_rxSchedulerService.GetUIScheduler())
.Publish(new List<IItemViewModel>()) .Publish(null)
.RefCount(); .RefCount();
CurrentSelectedItem = CurrentSelectedItem =
Observable.CombineLatest( Observable.CombineLatest(
CurrentItems, CurrentItems,
tab.CurrentSelectedItem, 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) .Publish(null)
.RefCount(); .RefCount();
var currentSelectedItemThrottled = CurrentSelectedItem.Throttle(TimeSpan.FromMilliseconds(250)).Publish(null).RefCount(); SelectedsChildren = InitSelectedsChildren();
SelectedsChildren = Observable.Merge( ParentsChildren = InitParentsChildren();
currentSelectedItemThrottled
.WhereNotNull()
.OfType<IContainerViewModel>()
.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<IItemViewModel>?)null)
)
.ObserveOn(_rxSchedulerService.GetWorkerScheduler())
.SubscribeOn(_rxSchedulerService.GetUIScheduler())
.Publish(null)
.RefCount();
var parentThrottled = CurrentLocation CurrentItemsCollectionObservable = CurrentItems
.Select(l => l?.Parent) .Select(c => c != null ? c.ToCollection() : Observable.Return((IReadOnlyCollection<IItemViewModel>?)null))
.DistinctUntilChanged() .Switch()
.Publish(null) .Publish(null)
.RefCount(); .RefCount();
ParentsChildren = Observable.Merge( CurrentItems.Subscribe(children =>
parentThrottled {
.Where(p => p is not null) CurrentItemsCollection?.Dispose();
.Select(p => Observable.FromAsync(async () => (IContainer)await p!.ResolveAsync())) CurrentItemsCollection = children.MapNull(c => new BindedCollection<IItemViewModel>(c!));
.Switch() });
.Select(p => p.Items)
.Switch() ParentsChildren.Subscribe(children =>
.Select(items => Observable.FromAsync(async () => await Map(items))) {
.Switch() ParentsChildrenCollection?.Dispose();
.Select(items => items?.Select(MapItemToViewModel).ToList()), ParentsChildrenCollection = children.MapNull(c => new BindedCollection<IItemViewModel>(c!));
parentThrottled });
.Where(p => p is null)
.Select(_ => (IReadOnlyList<IItemViewModel>?)null) SelectedsChildren.Subscribe(children =>
) {
.ObserveOn(_rxSchedulerService.GetWorkerScheduler()) SelectedsChildrenCollection?.Dispose();
.SubscribeOn(_rxSchedulerService.GetUIScheduler()) SelectedsChildrenCollection = children.MapNull(c => new BindedCollection<IItemViewModel>(c!));
.Publish(null) });
.RefCount();
tab.CurrentLocation.Subscribe((_) => _markedItems.OnNext(Enumerable.Empty<FullName>())); tab.CurrentLocation.Subscribe((_) => _markedItems.OnNext(Enumerable.Empty<FullName>()));
static async Task<List<IItem>?> Map(IEnumerable<IAbsolutePath>? items) IObservable<IObservable<IChangeSet<IItemViewModel>>?> InitSelectedsChildren()
{ {
if (items == null) return null; var currentSelectedItemThrottled = CurrentSelectedItem.Throttle(TimeSpan.FromMilliseconds(250)).Publish(null).RefCount();
return Observable.Merge(
currentSelectedItemThrottled
.WhereNotNull()
.OfType<IContainerViewModel>()
.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<IChangeSet<IItemViewModel>>?)null)
)
.ObserveOn(_rxSchedulerService.GetWorkerScheduler())
.SubscribeOn(_rxSchedulerService.GetUIScheduler())
.Publish(null)
.RefCount();
}
return await items IObservable<IObservable<IChangeSet<IItemViewModel>>?> InitParentsChildren()
.ToAsyncEnumerable() {
.SelectAwait(async i => await i.ResolveAsync(forceResolve: true, itemInitializationSettings: new ItemInitializationSettings(true))) var parentThrottled = CurrentLocation
.ToListAsync(); .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<IChangeSet<IItemViewModel>>?)null)
)
.ObserveOn(_rxSchedulerService.GetWorkerScheduler())
.SubscribeOn(_rxSchedulerService.GetUIScheduler())
.Publish(null)
.RefCount();
} }
} }
private IItemViewModel MapItemToViewModel(IItem item, int index) private static async Task<IItem> MapItem(IAbsolutePath item)
=> await item.ResolveAsync(forceResolve: true, itemInitializationSettings: new ItemInitializationSettings(true));
private IItemViewModel MapItemToViewModel(IItem item)
{ {
if (item is IContainer container) if (item is IContainer container)
{ {
var containerViewModel = _serviceProvider.GetInitableResolver<IContainer, ITabViewModel, int>(container, this, index).GetRequiredService<IContainerViewModel>(); var containerViewModel = _serviceProvider.GetInitableResolver<IContainer, ITabViewModel>(container, this).GetRequiredService<IContainerViewModel>();
return containerViewModel; return containerViewModel;
} }
else if (item is IFileElement fileElement) else if (item is IFileElement fileElement)
{ {
var fileViewModel = _serviceProvider.GetInitableResolver<IFileElement, ITabViewModel, int>(fileElement, this, index).GetRequiredService<IFileViewModel>(); var fileViewModel = _serviceProvider.GetInitableResolver<IFileElement, ITabViewModel>(fileElement, this).GetRequiredService<IFileViewModel>();
fileViewModel.Size = fileElement.Size; fileViewModel.Size = fileElement.Size;
return fileViewModel; return fileViewModel;
} }
else if (item is IElement element) else if (item is IElement element)
{ {
var elementViewModel = _serviceProvider.GetInitableResolver<IElement, ITabViewModel, int>(element, this, index).GetRequiredService<IElementViewModel>(); var elementViewModel = _serviceProvider.GetInitableResolver<IElement, ITabViewModel>(element, this).GetRequiredService<IElementViewModel>();
return elementViewModel; return elementViewModel;
} }

View File

@@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DynamicData" Version="7.6.7" />
<PackageReference Include="System.Reactive" Version="5.0.0" /> <PackageReference Include="System.Reactive" Version="5.0.0" />
</ItemGroup> </ItemGroup>

View File

@@ -1,8 +1,10 @@
using DynamicData;
namespace FileTime.Core.Models namespace FileTime.Core.Models
{ {
public interface IContainer : IItem public interface IContainer : IItem
{ {
IObservable<IEnumerable<IAbsolutePath>?> Items { get; } IObservable<IObservable<IChangeSet<IAbsolutePath>>?> Items { get; }
IObservable<bool> IsLoading { get; } IObservable<bool> IsLoading { get; }
} }
} }

View File

@@ -1,3 +1,4 @@
using DynamicData;
using FileTime.Core.Models; using FileTime.Core.Models;
using InitableService; using InitableService;
@@ -7,7 +8,7 @@ namespace FileTime.Core.Services
{ {
IObservable<IContainer?> CurrentLocation { get; } IObservable<IContainer?> CurrentLocation { get; }
IObservable<IAbsolutePath?> CurrentSelectedItem { get; } IObservable<IAbsolutePath?> CurrentSelectedItem { get; }
IObservable<IEnumerable<IItem>?> CurrentItems { get; } IObservable<IObservable<IChangeSet<IItem>>?> CurrentItems { get; }
void SetCurrentLocation(IContainer newLocation); void SetCurrentLocation(IContainer newLocation);
void AddSelectedItemsTransformator(ItemsTransformator transformator); void AddSelectedItemsTransformator(ItemsTransformator transformator);

View File

@@ -19,6 +19,14 @@ namespace FileTime.Core.Models
Type = type; 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<IItem> ResolveAsync(bool forceResolve = false, ItemInitializationSettings itemInitializationSettings = default) public async Task<IItem> ResolveAsync(bool forceResolve = false, ItemInitializationSettings itemInitializationSettings = default)
{ {
var provider = VirtualContentProvider ?? ContentProvider; var provider = VirtualContentProvider ?? ContentProvider;

View File

@@ -1,5 +1,6 @@
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using DynamicData;
using FileTime.Core.Enums; using FileTime.Core.Enums;
using FileTime.Core.Services; using FileTime.Core.Services;
@@ -19,7 +20,7 @@ namespace FileTime.Core.Models
string? Attributes, string? Attributes,
IContentProvider Provider, IContentProvider Provider,
IObservable<IEnumerable<Exception>> Exceptions, IObservable<IEnumerable<Exception>> Exceptions,
IObservable<IEnumerable<IAbsolutePath>?> Items) : IContainer IObservable<IObservable<IChangeSet<IAbsolutePath>>?> Items) : IContainer
{ {
BehaviorSubject<bool> IsLoading { get; } = new BehaviorSubject<bool>(false); BehaviorSubject<bool> IsLoading { get; } = new BehaviorSubject<bool>(false);
IObservable<bool> IContainer.IsLoading => IsLoading.AsObservable(); IObservable<bool> IContainer.IsLoading => IsLoading.AsObservable();

View File

@@ -1,5 +1,6 @@
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using DynamicData;
using FileTime.Core.Enums; using FileTime.Core.Enums;
using FileTime.Core.Models; using FileTime.Core.Models;
@@ -7,9 +8,9 @@ namespace FileTime.Core.Services
{ {
public abstract class ContentProviderBase : IContentProvider public abstract class ContentProviderBase : IContentProvider
{ {
protected BehaviorSubject<IReadOnlyList<IAbsolutePath>> Items { get; } = new BehaviorSubject<IReadOnlyList<IAbsolutePath>>(new List<IAbsolutePath>()); protected BehaviorSubject<IObservable<IChangeSet<IAbsolutePath>>?> Items { get; } = new (null);
IObservable<IEnumerable<IAbsolutePath>> IContainer.Items => Items; IObservable<IObservable<IChangeSet<IAbsolutePath>>?> IContainer.Items => Items;
public string Name { get; } public string Name { get; }

View File

@@ -13,6 +13,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Tools\FileTime.Tools\FileTime.Tools.csproj" />
<ProjectReference Include="..\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" /> <ProjectReference Include="..\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
<ProjectReference Include="..\FileTime.Core.Models\FileTime.Core.Models.csproj" /> <ProjectReference Include="..\FileTime.Core.Models\FileTime.Core.Models.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -1,5 +1,6 @@
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using DynamicData;
using FileTime.Core.Models; using FileTime.Core.Models;
namespace FileTime.Core.Services namespace FileTime.Core.Services
@@ -10,8 +11,9 @@ namespace FileTime.Core.Services
private readonly BehaviorSubject<IAbsolutePath?> _currentSelectedItem = new(null); private readonly BehaviorSubject<IAbsolutePath?> _currentSelectedItem = new(null);
private readonly List<ItemsTransformator> _transformators = new(); private readonly List<ItemsTransformator> _transformators = new();
private IAbsolutePath? _currentSelectedItemCached; private IAbsolutePath? _currentSelectedItemCached;
public IObservable<IContainer?> CurrentLocation { get; } public IObservable<IContainer?> CurrentLocation { get; }
public IObservable<IEnumerable<IItem>?> CurrentItems { get; } public IObservable<IObservable<IChangeSet<IItem>>?> CurrentItems { get; }
public IObservable<IAbsolutePath?> CurrentSelectedItem { get; } public IObservable<IAbsolutePath?> CurrentSelectedItem { get; }
public Tab() public Tab()
@@ -23,53 +25,54 @@ namespace FileTime.Core.Services
.Where(c => c is not null) .Where(c => c is not null)
.Select(c => c!.Items) .Select(c => c!.Items)
.Switch() .Switch()
.Select(i => i == null ? Observable.Return<IEnumerable<IItem>?>(null) : Observable.FromAsync(async () => await MapItems(i))) .Select(items => items?.TransformAsync(MapItem)),
.Switch(),
CurrentLocation CurrentLocation
.Where(c => c is null) .Where(c => c is null)
.Select(_ => Enumerable.Empty<IItem>()) .Select(_ => (IObservable<IChangeSet<IItem>>?)null)
) )
.Publish(Enumerable.Empty<IItem>()) .Publish((IObservable<IChangeSet<IItem>>?)null)
.RefCount(); .RefCount();
CurrentSelectedItem = CurrentLocation CurrentSelectedItem =
.Select(GetSelectedItemByLocation) Observable.CombineLatest(
.Switch() CurrentItems
.Merge(_currentSelectedItem) .Select(c =>
c == null
? Observable.Return<IReadOnlyCollection<IItem>?>(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() .DistinctUntilChanged()
.Publish(null) .Publish(null)
.RefCount(); .RefCount();
CurrentSelectedItem.Subscribe(s => _currentSelectedItemCached = s); CurrentSelectedItem.Subscribe(s =>
{
_currentSelectedItemCached = s;
_currentSelectedItem.OnNext(s);
});
} }
private async Task<IEnumerable<IItem>> MapItems(IEnumerable<IAbsolutePath> items) private async Task<IItem> MapItem(IAbsolutePath item) => await item.ResolveAsync(true);
{
IEnumerable<IItem> 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<IEnumerable<IItem>>(t.Transformator(acc)))
.ToListAsync()
)
.SelectMany(t => t);
}
public void Init(IContainer currentLocation) public void Init(IContainer currentLocation)
{ {
_currentLocation.OnNext(currentLocation); _currentLocation.OnNext(currentLocation);
} }
private IObservable<IAbsolutePath?> GetSelectedItemByLocation(IContainer? currentLocation) private static IAbsolutePath? GetSelectedItemByItems(IEnumerable<IItem> items)
{ {
//TODO: //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); public void SetCurrentLocation(IContainer newLocation) => _currentLocation.OnNext(newLocation);

View File

@@ -47,6 +47,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.Providers.Local.Ab
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.GuiApp.Abstractions", "GuiApp\Avalonia\FileTime.GuiApp.Abstractions\FileTime.GuiApp.Abstractions.csproj", "{D7D1C76A-05B0-49BC-BCFF-06340E264EC1}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.GuiApp.Abstractions", "GuiApp\Avalonia\FileTime.GuiApp.Abstractions\FileTime.GuiApp.Abstractions.csproj", "{D7D1C76A-05B0-49BC-BCFF-06340E264EC1}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{D7D1C76A-05B0-49BC-BCFF-06340E264EC1}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -134,6 +142,7 @@ Global
{4B742649-225F-4C73-B118-1B29FE2A5774} = {01F231DE-4A65-435F-B4BB-77EE5221890C} {4B742649-225F-4C73-B118-1B29FE2A5774} = {01F231DE-4A65-435F-B4BB-77EE5221890C}
{1500A537-2116-4111-B216-7632040619B0} = {2FC40FE1-4446-44AB-BF77-00F94D995FA3} {1500A537-2116-4111-B216-7632040619B0} = {2FC40FE1-4446-44AB-BF77-00F94D995FA3}
{D7D1C76A-05B0-49BC-BCFF-06340E264EC1} = {01F231DE-4A65-435F-B4BB-77EE5221890C} {D7D1C76A-05B0-49BC-BCFF-06340E264EC1} = {01F231DE-4A65-435F-B4BB-77EE5221890C}
{B7A45654-E56C-43C8-998E-0F4661395540} = {8C3CFEFE-78A5-4940-B388-D15FCE02ECE9}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}

View File

@@ -108,7 +108,7 @@
AutoScrollToSelectedItem="True" AutoScrollToSelectedItem="True"
Classes="ContentListView" Classes="ContentListView"
IsTabStop="True" IsTabStop="True"
Items="{Binding AppState.SelectedTab^.ParentsChildren^}" Items="{Binding AppState.SelectedTab^.ParentsChildrenCollection.Collection}"
ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Visible"> ScrollViewer.VerticalScrollBarVisibility="Visible">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
@@ -146,7 +146,7 @@
AutoScrollToSelectedItem="True" AutoScrollToSelectedItem="True"
Classes="ContentListView" Classes="ContentListView"
IsTabStop="True" IsTabStop="True"
Items="{Binding AppState.SelectedTab^.CurrentItems^}" Items="{Binding AppState.SelectedTab^.CurrentItemsCollection.Collection}"
ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Visible"> ScrollViewer.VerticalScrollBarVisibility="Visible">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
@@ -166,7 +166,7 @@
x:CompileBindings="False" x:CompileBindings="False"
FontWeight="Bold" FontWeight="Bold"
Foreground="{DynamicResource ErrorBrush}" 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 Empty
</TextBlock> </TextBlock>
</Grid> </Grid>
@@ -180,14 +180,14 @@
Fill="{DynamicResource ContentSeparatorBrush}" /> Fill="{DynamicResource ContentSeparatorBrush}" />
<Grid Grid.Column="4"> <Grid Grid.Column="4">
<Grid IsVisible="{Binding AppState.SelectedTab^.SelectedsChildren^, Converter={x:Static ObjectConverters.IsNotNull}}"> <Grid IsVisible="{Binding AppState.SelectedTab^.SelectedsChildrenCollection.Collection, Converter={x:Static ObjectConverters.IsNotNull}}">
<ListBox <ListBox
x:Name="ChildItems" x:Name="ChildItems"
x:CompileBindings="False" x:CompileBindings="False"
AutoScrollToSelectedItem="True" AutoScrollToSelectedItem="True"
Classes="ContentListView" Classes="ContentListView"
IsVisible="{Binding AppState.SelectedTab^.SelectedsChildren^.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}" IsVisible="{Binding AppState.SelectedTab^.SelectedsChildrenCollection.Collection.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}"
Items="{Binding AppState.SelectedTab^.SelectedsChildren^}"> Items="{Binding AppState.SelectedTab^.SelectedsChildrenCollection.Collection}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="corevm:IItemViewModel"> <DataTemplate x:DataType="corevm:IItemViewModel">
<local:ItemView /> <local:ItemView />
@@ -202,13 +202,13 @@
x:CompileBindings="False" x:CompileBindings="False"
FontWeight="Bold" FontWeight="Bold"
Foreground="{DynamicResource ErrorBrush}" 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 Empty
</TextBlock> </TextBlock>
</Grid> </Grid>
<Grid <Grid
IsVisible="{Binding AppState.SelectedTab^.SelectedsChildren^, Converter={x:Static ObjectConverters.IsNull}, ConverterParameter=0}" IsVisible="{Binding AppState.SelectedTab^.SelectedsChildrenCollection, Converter={x:Static ObjectConverters.IsNull}, ConverterParameter=0}"
RowDefinitions="Auto, Auto"> RowDefinitions="Auto, Auto">
<TextBlock <TextBlock
Margin="0,0,0,10" Margin="0,0,0,10"

View File

@@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DynamicData" Version="7.6.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
</ItemGroup> </ItemGroup>

View File

@@ -1,19 +1,24 @@
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using DynamicData;
using FileTime.Core.Enums; using FileTime.Core.Enums;
using FileTime.Core.Models; using FileTime.Core.Models;
using FileTime.Core.Services; using FileTime.Core.Services;
namespace FileTime.Providers.Local 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<IAbsolutePath> _rootDirectories = new();
private readonly bool _isCaseInsensitive;
public LocalContentProvider() : base("local") public LocalContentProvider() : base("local")
{ {
IsCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); _isCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
RefreshRootDirectories(); RefreshRootDirectories();
Items.OnNext(_rootDirectories.Connect());
} }
public override Task OnEnter() public override Task OnEnter()
@@ -29,7 +34,11 @@ namespace FileTime.Providers.Local
? new DirectoryInfo("/").GetDirectories() ? new DirectoryInfo("/").GetDirectories()
: Environment.GetLogicalDrives().Select(d => new DirectoryInfo(d)); : 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<IItem> GetItemByNativePathAsync( public override Task<IItem> GetItemByNativePathAsync(
@@ -108,7 +117,7 @@ namespace FileTime.Providers.Local
"???", "???",
this, this,
nonNullExceptions, nonNullExceptions,
Observable.Return<IEnumerable<IAbsolutePath>?>(null) Observable.Return<IObservable<IChangeSet<IAbsolutePath>>?>(null)
); );
} }
@@ -160,19 +169,24 @@ namespace FileTime.Providers.Local
Observable.FromAsync(async () => await Task.Run(InitChildren)) Observable.FromAsync(async () => await Task.Run(InitChildren))
); );
Task<List<IAbsolutePath>?> InitChildren() Task<IObservable<IChangeSet<IAbsolutePath>>?> InitChildren()
{ {
List<IAbsolutePath>? result = null; SourceList<IAbsolutePath>? result = null;
try try
{ {
result = initializeChildren ? (List<IAbsolutePath>?)GetItemsByContainer(directoryInfo) : null; var items = initializeChildren ? (List<IAbsolutePath>?)GetItemsByContainer(directoryInfo) : null;
if (items != null)
{
result = new SourceList<IAbsolutePath>();
result.AddRange(items);
}
} }
catch (Exception e) catch (Exception e)
{ {
exceptions.OnNext(new List<Exception>() { e }); exceptions.OnNext(new List<Exception>() { e });
} }
return Task.FromResult(result); return Task.FromResult(result?.Connect());
} }
} }

View File

@@ -0,0 +1,11 @@
namespace FileTime.Tools.Extensions
{
public static class ObjectExtensions
{
public static TResult? MapNull<T, TResult>(this T obj, Func<TResult?> nullHandler, Func<T, TResult?> valueHandler)
=> obj == null ? nullHandler() : valueHandler(obj);
public static TResult? MapNull<T, TResult>(this T obj, Func<T, TResult?> valueHandler)
=> obj == null ? default : valueHandler(obj);
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>