New reactive core WIP
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
namespace FileTime.App.Core.Services;
|
||||||
|
|
||||||
|
public interface IRefreshSmoothnessCalculator
|
||||||
|
{
|
||||||
|
TimeSpan RefreshDelay { get; }
|
||||||
|
void RegisterChange();
|
||||||
|
void RegisterChange(DateTime changeTime);
|
||||||
|
void RecalculateSmoothness();
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using DeclarativeProperty;
|
||||||
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;
|
||||||
@@ -10,9 +11,9 @@ public interface IItemViewModel : IInitable<IItem, ITabViewModel, ItemViewModelT
|
|||||||
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; }
|
IDeclarativeProperty<bool> IsSelected { get; set; }
|
||||||
IObservable<bool>? IsMarked { get; set; }
|
IObservable<bool>? IsMarked { get; set; }
|
||||||
IObservable<bool> IsAlternative { get; }
|
IDeclarativeProperty<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; }
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using DeclarativeProperty;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
using FileTime.Core.Services;
|
using FileTime.Core.Services;
|
||||||
@@ -11,19 +13,13 @@ public interface ITabViewModel : IInitable<ITab, int>, IDisposable
|
|||||||
ITab? Tab { get; }
|
ITab? Tab { get; }
|
||||||
int TabNumber { get; }
|
int TabNumber { get; }
|
||||||
IObservable<bool> IsSelected { get; }
|
IObservable<bool> IsSelected { get; }
|
||||||
IObservable<IContainer?> CurrentLocation { get; }
|
IDeclarativeProperty<IContainer?> CurrentLocation { get; }
|
||||||
IObservable<IItemViewModel?> CurrentSelectedItem { get; }
|
IDeclarativeProperty<IItemViewModel?> CurrentSelectedItem { get; }
|
||||||
IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> CurrentItems { get; }
|
IDeclarativeProperty<IContainerViewModel?> CurrentSelectedItemAsContainer { get; }
|
||||||
|
IDeclarativeProperty<ObservableCollection<IItemViewModel>?> CurrentItems { get; }
|
||||||
IObservable<IChangeSet<FullName>> MarkedItems { get; }
|
IObservable<IChangeSet<FullName>> MarkedItems { get; }
|
||||||
IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> SelectedsChildren { get; }
|
IDeclarativeProperty<ObservableCollection<IItemViewModel>> SelectedsChildren { get; }
|
||||||
IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> ParentsChildren { get; }
|
IDeclarativeProperty<ObservableCollection<IItemViewModel>> ParentsChildren { get; }
|
||||||
BindedCollection<IItemViewModel, string>? CurrentItemsCollection { get; }
|
|
||||||
BindedCollection<IItemViewModel, string>? SelectedsChildrenCollection { get; }
|
|
||||||
BindedCollection<IItemViewModel, string>? ParentsChildrenCollection { get; }
|
|
||||||
IObservable<IReadOnlyCollection<IItemViewModel>?> CurrentItemsCollectionObservable { get; }
|
|
||||||
IObservable<IReadOnlyCollection<IItemViewModel>?> ParentsChildrenCollectionObservable { get; }
|
|
||||||
IObservable<IReadOnlyCollection<IItemViewModel>?> SelectedsChildrenCollectionObservable { get; }
|
|
||||||
IContainer? CachedCurrentLocation { get; }
|
|
||||||
|
|
||||||
void ClearMarkedItems();
|
void ClearMarkedItems();
|
||||||
void RemoveMarkedItem(FullName fullName);
|
void RemoveMarkedItem(FullName fullName);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
||||||
<PackageReference Include="morelinq" Version="3.4.2" />
|
<PackageReference Include="morelinq" Version="3.4.2" />
|
||||||
<PackageReference Include="MvvmGen" Version="1.2.1" />
|
<PackageReference Include="MvvmGen" Version="1.2.1" />
|
||||||
|
<PackageReference Include="ObservableComputations" Version="2.3.0" />
|
||||||
<PackageReference Include="System.Interactive.Async" Version="6.0.1" />
|
<PackageReference Include="System.Interactive.Async" Version="6.0.1" />
|
||||||
<PackageReference Include="System.Reactive" Version="6.0.0" />
|
<PackageReference Include="System.Reactive" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -21,6 +22,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="..\..\Library\Defer\Defer.csproj" />
|
||||||
<ProjectReference Include="..\..\Providers\FileTime.Providers.Local.Abstractions\FileTime.Providers.Local.Abstractions.csproj" />
|
<ProjectReference Include="..\..\Providers\FileTime.Providers.Local.Abstractions\FileTime.Providers.Local.Abstractions.csproj" />
|
||||||
<ProjectReference Include="..\..\Tools\FileTime.Tools\FileTime.Tools.csproj" />
|
<ProjectReference Include="..\..\Tools\FileTime.Tools\FileTime.Tools.csproj" />
|
||||||
<ProjectReference Include="..\FileTime.App.CommandPalette.Abstractions\FileTime.App.CommandPalette.Abstractions.csproj" />
|
<ProjectReference Include="..\FileTime.App.CommandPalette.Abstractions\FileTime.App.CommandPalette.Abstractions.csproj" />
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ public class TabPersistenceService : ITabPersistenceService
|
|||||||
{
|
{
|
||||||
if (!File.Exists(_settingsPath))
|
if (!File.Exists(_settingsPath))
|
||||||
{
|
{
|
||||||
CreateEmptyTab();
|
await CreateEmptyTab();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,12 +99,12 @@ public class TabPersistenceService : ITabPersistenceService
|
|||||||
_logger.LogError(e, "Unknown exception while restoring app state");
|
_logger.LogError(e, "Unknown exception while restoring app state");
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateEmptyTab();
|
await CreateEmptyTab();
|
||||||
|
|
||||||
void CreateEmptyTab()
|
async Task CreateEmptyTab()
|
||||||
{
|
{
|
||||||
var tab = _serviceProvider.GetInitableResolver<IContainer>(_localContentProvider)
|
var tab = await _serviceProvider.GetAsyncInitableResolver<IContainer>(_localContentProvider)
|
||||||
.GetRequiredService<ITab>();
|
.GetRequiredServiceAsync<ITab>();
|
||||||
var tabViewModel = _serviceProvider.GetInitableResolver(tab, 1).GetRequiredService<ITabViewModel>();
|
var tabViewModel = _serviceProvider.GetInitableResolver(tab, 1).GetRequiredService<ITabViewModel>();
|
||||||
|
|
||||||
_appState.AddTab(tabViewModel);
|
_appState.AddTab(tabViewModel);
|
||||||
@@ -159,8 +159,8 @@ public class TabPersistenceService : ITabPersistenceService
|
|||||||
|
|
||||||
if (container == null) continue;
|
if (container == null) continue;
|
||||||
|
|
||||||
var tabToLoad = _serviceProvider.GetInitableResolver(container)
|
var tabToLoad = await _serviceProvider.GetAsyncInitableResolver(container)
|
||||||
.GetRequiredService<ITab>();
|
.GetRequiredServiceAsync<ITab>();
|
||||||
var tabViewModel = _serviceProvider.GetInitableResolver(tabToLoad, tab.Number)
|
var tabViewModel = _serviceProvider.GetInitableResolver(tabToLoad, tab.Number)
|
||||||
.GetRequiredService<ITabViewModel>();
|
.GetRequiredService<ITabViewModel>();
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ public class TabPersistenceService : ITabPersistenceService
|
|||||||
var tabStates = new List<TabState>();
|
var tabStates = new List<TabState>();
|
||||||
foreach (var tab in _appState.Tabs)
|
foreach (var tab in _appState.Tabs)
|
||||||
{
|
{
|
||||||
var currentLocation = tab.CachedCurrentLocation;
|
var currentLocation = tab.CurrentLocation.Value;
|
||||||
if (currentLocation is null) continue;
|
if (currentLocation is null) continue;
|
||||||
tabStates.Add(new TabState(currentLocation.FullName!, tab.TabNumber));
|
tabStates.Add(new TabState(currentLocation.FullName!, tab.TabNumber));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace FileTime.App.Core.Services;
|
||||||
|
|
||||||
|
public sealed class RefreshSmoothnessCalculator : IRefreshSmoothnessCalculator, INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private const int MaxSampleTimeInSeconds = 10;
|
||||||
|
private const int MaxDelayBetweenRefreshes = 400;
|
||||||
|
private readonly TimeSpan _maxDelay = TimeSpan.FromSeconds(MaxSampleTimeInSeconds);
|
||||||
|
private readonly TimeSpan _defaultRefreshDelay = TimeSpan.FromMilliseconds(200);
|
||||||
|
private readonly Queue<DateTime> _changeTimes = new();
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private TimeSpan _refreshDelay;
|
||||||
|
|
||||||
|
public TimeSpan RefreshDelay
|
||||||
|
{
|
||||||
|
get => _refreshDelay;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (value.Equals(_refreshDelay)) return;
|
||||||
|
_refreshDelay = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RefreshSmoothnessCalculator()
|
||||||
|
{
|
||||||
|
_refreshDelay = _defaultRefreshDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterChange() => RegisterChange(DateTime.Now);
|
||||||
|
|
||||||
|
public void RegisterChange(DateTime changeTime)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_changeTimes.Count > 0 && Math.Abs((_changeTimes.Last() - changeTime).TotalMilliseconds) < 5) return;
|
||||||
|
CleanList(DateTime.Now);
|
||||||
|
_changeTimes.Enqueue(changeTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanList(DateTime now)
|
||||||
|
{
|
||||||
|
while (_changeTimes.Count > 0)
|
||||||
|
{
|
||||||
|
var item = _changeTimes.Peek();
|
||||||
|
if (now - item < _maxDelay) return;
|
||||||
|
_changeTimes.Dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecalculateSmoothness()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_changeTimes.Count < 2)
|
||||||
|
{
|
||||||
|
RefreshDelay = _defaultRefreshDelay;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.Now;
|
||||||
|
CleanList(now);
|
||||||
|
|
||||||
|
var queue = new Queue<DateTime>(_changeTimes);
|
||||||
|
var values = new List<(double score, double weight)>(queue.Count - 1);
|
||||||
|
|
||||||
|
var previousChangeTime = queue.Dequeue();
|
||||||
|
var biggestDelay = now - previousChangeTime;
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var changeTime = queue.Dequeue();
|
||||||
|
|
||||||
|
var (score, weight) = CalculateScoreAndWeight(changeTime, previousChangeTime, biggestDelay);
|
||||||
|
|
||||||
|
values.Add((score, weight));
|
||||||
|
|
||||||
|
previousChangeTime = changeTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
var combinedScore = values.Sum(i => i.weight * i.score) / values.Sum(i => i.weight);
|
||||||
|
|
||||||
|
var normalizedCombinedScore = (combinedScore * 1.2 - 0.1);
|
||||||
|
|
||||||
|
if (normalizedCombinedScore < 0) normalizedCombinedScore = 0;
|
||||||
|
else if (normalizedCombinedScore > 1) normalizedCombinedScore = 1;
|
||||||
|
|
||||||
|
var finalDelay = normalizedCombinedScore * MaxDelayBetweenRefreshes;
|
||||||
|
|
||||||
|
RefreshDelay = TimeSpan.FromMilliseconds(finalDelay);
|
||||||
|
|
||||||
|
(double score, double weight) CalculateScoreAndWeight(DateTime changeTime, DateTime previousChangeTime, TimeSpan biggestDelay)
|
||||||
|
{
|
||||||
|
var delayToPrevious = changeTime - previousChangeTime;
|
||||||
|
var delayToNow = now - changeTime;
|
||||||
|
|
||||||
|
var toNowRatio = (delayToNow.TotalMilliseconds / biggestDelay.TotalMilliseconds);
|
||||||
|
var score = 1 - (delayToPrevious.TotalMilliseconds / biggestDelay.TotalMilliseconds);
|
||||||
|
var weight = 1 - toNowRatio;
|
||||||
|
|
||||||
|
if (score < 0) score = 0;
|
||||||
|
else if (score > 1) score = 1;
|
||||||
|
|
||||||
|
return (score, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Reactive.Subjects;
|
using System.Reactive.Subjects;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using DeclarativeProperty;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using FileTime.App.Core.Interactions;
|
using FileTime.App.Core.Interactions;
|
||||||
using FileTime.App.Core.Models;
|
using FileTime.App.Core.Models;
|
||||||
@@ -25,7 +26,7 @@ namespace FileTime.App.Core.Services.UserCommandHandler;
|
|||||||
public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServiceBase
|
public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServiceBase
|
||||||
{
|
{
|
||||||
private ITabViewModel? _selectedTab;
|
private ITabViewModel? _selectedTab;
|
||||||
private IItemViewModel? _currentSelectedItem;
|
private IDeclarativeProperty<IItemViewModel?>? _currentSelectedItem;
|
||||||
private readonly IUserCommandHandlerService _userCommandHandlerService;
|
private readonly IUserCommandHandlerService _userCommandHandlerService;
|
||||||
private readonly IClipboardService _clipboardService;
|
private readonly IClipboardService _clipboardService;
|
||||||
private readonly IUserCommunicationService _userCommunicationService;
|
private readonly IUserCommunicationService _userCommunicationService;
|
||||||
@@ -35,7 +36,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
|||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ISystemClipboardService _systemClipboardService;
|
private readonly ISystemClipboardService _systemClipboardService;
|
||||||
private readonly BindedCollection<FullName>? _markedItems;
|
private readonly BindedCollection<FullName>? _markedItems;
|
||||||
private IContainer? _currentLocation;
|
private IDeclarativeProperty<IContainer?>? _currentLocation;
|
||||||
|
|
||||||
public ItemManipulationUserCommandHandlerService(
|
public ItemManipulationUserCommandHandlerService(
|
||||||
IAppState appState,
|
IAppState appState,
|
||||||
@@ -84,7 +85,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
|||||||
{
|
{
|
||||||
list.AddRange(_markedItems!.Collection!);
|
list.AddRange(_markedItems!.Collection!);
|
||||||
}
|
}
|
||||||
else if(_currentSelectedItem?.BaseItem?.FullName is { } selectedItemName)
|
else if(_currentSelectedItem?.Value?.BaseItem?.FullName is { } selectedItemName)
|
||||||
{
|
{
|
||||||
list.Add(selectedItemName);
|
list.Add(selectedItemName);
|
||||||
}
|
}
|
||||||
@@ -106,20 +107,20 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
|||||||
|
|
||||||
private async Task PasteFilesFromClipboardAsync(TransportMode mode)
|
private async Task PasteFilesFromClipboardAsync(TransportMode mode)
|
||||||
{
|
{
|
||||||
if (_currentLocation?.FullName is not { }) return;
|
if (_currentLocation?.Value?.FullName is not { }) return;
|
||||||
|
|
||||||
var files = (await _systemClipboardService.GetFilesAsync()).ToList();
|
var files = (await _systemClipboardService.GetFilesAsync()).ToList();
|
||||||
var copyCommandFactory = _serviceProvider.GetRequiredService<FileTime.Core.Command.Copy.CopyCommandFactory>();
|
var copyCommandFactory = _serviceProvider.GetRequiredService<FileTime.Core.Command.Copy.CopyCommandFactory>();
|
||||||
var copyCommand = copyCommandFactory.GenerateCommand(files, mode, _currentLocation.FullName);
|
var copyCommand = copyCommandFactory.GenerateCommand(files, mode, _currentLocation.Value.FullName);
|
||||||
|
|
||||||
await AddCommandAsync(copyCommand);
|
await AddCommandAsync(copyCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task MarkItemAsync()
|
private async Task MarkItemAsync()
|
||||||
{
|
{
|
||||||
if (_selectedTab == null || _currentSelectedItem?.BaseItem?.FullName == null) return;
|
if (_selectedTab == null || _currentSelectedItem?.Value?.BaseItem?.FullName == null) return;
|
||||||
|
|
||||||
_selectedTab.ToggleMarkedItem(_currentSelectedItem.BaseItem.FullName);
|
_selectedTab.ToggleMarkedItem(_currentSelectedItem.Value.BaseItem.FullName);
|
||||||
await _userCommandHandlerService.HandleCommandAsync(MoveCursorDownCommand.Instance);
|
await _userCommandHandlerService.HandleCommandAsync(MoveCursorDownCommand.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,9 +138,9 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
|||||||
|
|
||||||
_selectedTab?.ClearMarkedItems();
|
_selectedTab?.ClearMarkedItems();
|
||||||
}
|
}
|
||||||
else if (_currentSelectedItem?.BaseItem != null)
|
else if (_currentSelectedItem?.Value?.BaseItem != null)
|
||||||
{
|
{
|
||||||
var item = _currentSelectedItem.BaseItem;
|
var item = _currentSelectedItem.Value.BaseItem;
|
||||||
_clipboardService.AddContent(
|
_clipboardService.AddContent(
|
||||||
item.FullName
|
item.FullName
|
||||||
?? throw new ArgumentException($"{nameof(item.FullName)} can not be null.", nameof(item))
|
?? throw new ArgumentException($"{nameof(item.FullName)} can not be null.", nameof(item))
|
||||||
@@ -168,7 +169,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
|||||||
|
|
||||||
//TODO: check _currentLocation?.FullName
|
//TODO: check _currentLocation?.FullName
|
||||||
var commandFactory = (ITransportationCommandFactory) _serviceProvider.GetRequiredService(_clipboardService.CommandFactoryType);
|
var commandFactory = (ITransportationCommandFactory) _serviceProvider.GetRequiredService(_clipboardService.CommandFactoryType);
|
||||||
var command = commandFactory.GenerateCommand(_clipboardService.Content, mode, _currentLocation?.FullName);
|
var command = commandFactory.GenerateCommand(_clipboardService.Content, mode, _currentLocation?.Value?.FullName);
|
||||||
|
|
||||||
_clipboardService.Clear();
|
_clipboardService.Clear();
|
||||||
|
|
||||||
@@ -186,10 +187,10 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
|||||||
//TODO: message on empty result
|
//TODO: message on empty result
|
||||||
var newContainerName = containerNameInput.Value;
|
var newContainerName = containerNameInput.Value;
|
||||||
|
|
||||||
if (_currentLocation?.FullName is null || newContainerName is null) return;
|
if (_currentLocation?.Value?.FullName is null || newContainerName is null) return;
|
||||||
|
|
||||||
var command = _serviceProvider
|
var command = _serviceProvider
|
||||||
.GetInitableResolver(_currentLocation.FullName, newContainerName)
|
.GetInitableResolver(_currentLocation.Value.FullName, newContainerName)
|
||||||
.GetRequiredService<CreateContainerCommand>();
|
.GetRequiredService<CreateContainerCommand>();
|
||||||
await AddCommandAsync(command);
|
await AddCommandAsync(command);
|
||||||
}
|
}
|
||||||
@@ -203,10 +204,10 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
|||||||
//TODO: message on empty result
|
//TODO: message on empty result
|
||||||
var newContainerName = containerNameInput.Value;
|
var newContainerName = containerNameInput.Value;
|
||||||
|
|
||||||
if (_currentLocation?.FullName is null || newContainerName is null) return;
|
if (_currentLocation?.Value?.FullName is null || newContainerName is null) return;
|
||||||
|
|
||||||
var command = _serviceProvider
|
var command = _serviceProvider
|
||||||
.GetInitableResolver(_currentLocation.FullName, newContainerName)
|
.GetInitableResolver(_currentLocation.Value.FullName, newContainerName)
|
||||||
.GetRequiredService<FileTime.Core.Command.CreateElement.CreateElementCommand>();
|
.GetRequiredService<FileTime.Core.Command.CreateElement.CreateElementCommand>();
|
||||||
await AddCommandAsync(command);
|
await AddCommandAsync(command);
|
||||||
}
|
}
|
||||||
@@ -342,9 +343,9 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_currentSelectedItem?.BaseItem?.FullName is null) return;
|
if (_currentSelectedItem?.Value?.BaseItem?.FullName is null) return;
|
||||||
|
|
||||||
var item = await _timelessContentProvider.GetItemByFullNameAsync(_currentSelectedItem.BaseItem.FullName, PointInTime.Present);
|
var item = await _timelessContentProvider.GetItemByFullNameAsync(_currentSelectedItem.Value.BaseItem.FullName, PointInTime.Present);
|
||||||
|
|
||||||
if (item is null) return;
|
if (item is null) return;
|
||||||
|
|
||||||
@@ -410,11 +411,11 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
|||||||
{
|
{
|
||||||
itemsToDelete = new List<FullName>(_markedItems!.Collection!);
|
itemsToDelete = new List<FullName>(_markedItems!.Collection!);
|
||||||
}
|
}
|
||||||
else if (_currentSelectedItem?.BaseItem?.FullName is not null)
|
else if (_currentSelectedItem?.Value?.BaseItem?.FullName is not null)
|
||||||
{
|
{
|
||||||
itemsToDelete = new List<FullName>()
|
itemsToDelete = new List<FullName>()
|
||||||
{
|
{
|
||||||
_currentSelectedItem.BaseItem.FullName
|
_currentSelectedItem.Value.BaseItem.FullName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,7 +426,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
|||||||
var resolvedOnlyItem = await _timelessContentProvider.GetItemByFullNameAsync(itemsToDelete[0], PointInTime.Present);
|
var resolvedOnlyItem = await _timelessContentProvider.GetItemByFullNameAsync(itemsToDelete[0], PointInTime.Present);
|
||||||
|
|
||||||
if (resolvedOnlyItem is IContainer {AllowRecursiveDeletion: true} onlyContainer
|
if (resolvedOnlyItem is IContainer {AllowRecursiveDeletion: true} onlyContainer
|
||||||
&& onlyContainer.ItemsCollection.Any())
|
&& onlyContainer.Items.Count > 0)
|
||||||
{
|
{
|
||||||
questionText = $"The container '{onlyContainer.DisplayName}' is not empty. Proceed with delete?";
|
questionText = $"The container '{onlyContainer.DisplayName}' is not empty. Proceed with delete?";
|
||||||
}
|
}
|
||||||
@@ -462,7 +463,5 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddCommandAsync(ICommand command)
|
private async Task AddCommandAsync(ICommand command)
|
||||||
{
|
=> await _commandScheduler.AddCommand(command);
|
||||||
await _commandScheduler.AddCommand(command);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using DeclarativeProperty;
|
||||||
using FileTime.App.CommandPalette.Services;
|
using FileTime.App.CommandPalette.Services;
|
||||||
using FileTime.App.Core.Extensions;
|
using FileTime.App.Core.Extensions;
|
||||||
using FileTime.App.Core.Models.Enums;
|
using FileTime.App.Core.Models.Enums;
|
||||||
@@ -25,9 +27,9 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
|||||||
private readonly IFrequencyNavigationService _frequencyNavigationService;
|
private readonly IFrequencyNavigationService _frequencyNavigationService;
|
||||||
private readonly ICommandPaletteService _commandPaletteService;
|
private readonly ICommandPaletteService _commandPaletteService;
|
||||||
private ITabViewModel? _selectedTab;
|
private ITabViewModel? _selectedTab;
|
||||||
private IContainer? _currentLocation;
|
private IDeclarativeProperty<IContainer?>? _currentLocation;
|
||||||
private IItemViewModel? _currentSelectedItem;
|
private IDeclarativeProperty<IItemViewModel?>? _currentSelectedItem;
|
||||||
private IEnumerable<IItemViewModel> _currentItems = Enumerable.Empty<IItemViewModel>();
|
private IDeclarativeProperty<ObservableCollection<IItemViewModel>?>? _currentItems;
|
||||||
private ViewMode _viewMode;
|
private ViewMode _viewMode;
|
||||||
|
|
||||||
public NavigationUserCommandHandlerService(
|
public NavigationUserCommandHandlerService(
|
||||||
@@ -121,7 +123,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
|||||||
|
|
||||||
private async Task GoToRoot()
|
private async Task GoToRoot()
|
||||||
{
|
{
|
||||||
var root = _currentLocation;
|
var root = _currentLocation?.Value;
|
||||||
if (root is null) return;
|
if (root is null) return;
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
@@ -138,21 +140,25 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
|||||||
|
|
||||||
private async Task GoToProvider()
|
private async Task GoToProvider()
|
||||||
{
|
{
|
||||||
if (_currentLocation is null) return;
|
if (_currentLocation?.Value is null) return;
|
||||||
|
|
||||||
await _userCommandHandlerService.HandleCommandAsync(
|
await _userCommandHandlerService.HandleCommandAsync(
|
||||||
new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, _currentLocation.Provider)));
|
new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, _currentLocation.Value.Provider)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Refresh()
|
private async Task Refresh()
|
||||||
{
|
{
|
||||||
if (_currentLocation?.FullName is null) return;
|
if (_currentLocation?.Value?.FullName is null) return;
|
||||||
var refreshedItem =
|
var refreshedItem =
|
||||||
await _timelessContentProvider.GetItemByFullNameAsync(_currentLocation.FullName, PointInTime.Present);
|
await _timelessContentProvider.GetItemByFullNameAsync(_currentLocation.Value.FullName, PointInTime.Present);
|
||||||
|
|
||||||
if (refreshedItem is not IContainer refreshedContainer) return;
|
if (refreshedItem is not IContainer refreshedContainer) return;
|
||||||
|
|
||||||
_selectedTab?.Tab?.ForceSetCurrentLocation(refreshedContainer);
|
|
||||||
|
if (_selectedTab?.Tab is { } tab)
|
||||||
|
{
|
||||||
|
await tab.ForceSetCurrentLocation(refreshedContainer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenContainer(OpenContainerCommand command)
|
private async Task OpenContainer(OpenContainerCommand command)
|
||||||
@@ -160,91 +166,100 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
|||||||
var resolvedPath = await command.Path.ResolveAsync();
|
var resolvedPath = await command.Path.ResolveAsync();
|
||||||
if (resolvedPath is not IContainer resolvedContainer) return;
|
if (resolvedPath is not IContainer resolvedContainer) return;
|
||||||
|
|
||||||
_selectedTab?.Tab?.SetCurrentLocation(resolvedContainer);
|
if (_selectedTab?.Tab is { } tab)
|
||||||
|
{
|
||||||
|
await tab.SetCurrentLocation(resolvedContainer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task OpenSelected()
|
private async Task OpenSelected()
|
||||||
{
|
{
|
||||||
if (_currentSelectedItem is not IContainerViewModel containerViewModel || containerViewModel.Container is null)
|
if (_currentSelectedItem?.Value is not IContainerViewModel containerViewModel || containerViewModel.Container is null)
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
|
|
||||||
_appState.RapidTravelText = "";
|
_appState.RapidTravelText = "";
|
||||||
_selectedTab?.Tab?.SetCurrentLocation(containerViewModel.Container);
|
if (_selectedTab?.Tab is { } tab)
|
||||||
return Task.CompletedTask;
|
{
|
||||||
|
await tab.SetCurrentLocation(containerViewModel.Container);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GoUp()
|
private async Task GoUp()
|
||||||
{
|
{
|
||||||
if (_currentLocation?.Parent is not AbsolutePath parentPath ||
|
if (_currentLocation?.Value?.Parent is not AbsolutePath parentPath ||
|
||||||
await parentPath.ResolveAsyncSafe() is not IContainer newContainer)
|
await parentPath.ResolveAsyncSafe() is not IContainer newContainer)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_appState.RapidTravelText = "";
|
_appState.RapidTravelText = "";
|
||||||
_selectedTab?.Tab?.SetCurrentLocation(newContainer);
|
if (_selectedTab?.Tab is { } tab)
|
||||||
|
{
|
||||||
|
await tab.SetCurrentLocation(newContainer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task MoveCursorDown()
|
private async Task MoveCursorDown()
|
||||||
|
=> await SelectNewSelectedItem(items =>
|
||||||
{
|
{
|
||||||
SelectNewSelectedItem(items =>
|
if (_currentSelectedItem?.Value == null) return items.FirstOrDefault();
|
||||||
items.SkipWhile(i => !i.EqualsTo(_currentSelectedItem)).Skip(1).FirstOrDefault());
|
return items.SkipWhile(i => !i.EqualsTo(_currentSelectedItem?.Value)).Skip(1).FirstOrDefault();
|
||||||
return Task.CompletedTask;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
private Task MoveCursorUp()
|
private async Task MoveCursorUp()
|
||||||
|
=> await SelectNewSelectedItem(items =>
|
||||||
{
|
{
|
||||||
SelectNewSelectedItem(items => items.TakeWhile(i => !i.EqualsTo(_currentSelectedItem)).LastOrDefault());
|
if (_currentSelectedItem?.Value == null) return items.LastOrDefault();
|
||||||
return Task.CompletedTask;
|
return items.TakeWhile(i => !i.EqualsTo(_currentSelectedItem?.Value)).LastOrDefault();
|
||||||
}
|
});
|
||||||
|
|
||||||
private Task MoveCursorDownPage()
|
private async Task MoveCursorDownPage()
|
||||||
|
=> await SelectNewSelectedItem(items =>
|
||||||
{
|
{
|
||||||
SelectNewSelectedItem(items =>
|
var relevantItems = _currentSelectedItem?.Value is null
|
||||||
{
|
? items.ToList()
|
||||||
var relevantItems = items.SkipWhile(i => !i.EqualsTo(_currentSelectedItem)).ToList();
|
: items.SkipWhile(i => !i.EqualsTo(_currentSelectedItem.Value)).ToList();
|
||||||
|
|
||||||
var fallBackItems = relevantItems.Take(PageSize + 1).Reverse();
|
var fallBackItems = relevantItems.Take(PageSize + 1).Reverse();
|
||||||
var preferredItems = relevantItems.Skip(PageSize + 1);
|
var preferredItems = relevantItems.Skip(PageSize + 1);
|
||||||
|
|
||||||
return preferredItems.Concat(fallBackItems).FirstOrDefault();
|
return preferredItems.Concat(fallBackItems).FirstOrDefault();
|
||||||
});
|
});
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task MoveCursorUpPage()
|
private async Task MoveCursorUpPage()
|
||||||
|
=> await SelectNewSelectedItem(items =>
|
||||||
{
|
{
|
||||||
SelectNewSelectedItem(items =>
|
var relevantItems = _currentSelectedItem?.Value is null
|
||||||
{
|
? items.Reverse().ToList()
|
||||||
var relevantItems = items.TakeWhile(i => !i.EqualsTo(_currentSelectedItem)).Reverse().ToList();
|
: items.TakeWhile(i => !i.EqualsTo(_currentSelectedItem?.Value)).Reverse().ToList();
|
||||||
|
|
||||||
var fallBackItems = relevantItems.Take(PageSize).Reverse();
|
var fallBackItems = relevantItems.Take(PageSize).Reverse();
|
||||||
var preferredItems = relevantItems.Skip(PageSize);
|
var preferredItems = relevantItems.Skip(PageSize);
|
||||||
|
|
||||||
return preferredItems.Concat(fallBackItems).FirstOrDefault();
|
return preferredItems.Concat(fallBackItems).FirstOrDefault();
|
||||||
});
|
});
|
||||||
return Task.CompletedTask;
|
|
||||||
|
private async Task MoveCursorToFirst()
|
||||||
|
=> await SelectNewSelectedItem(items => items.FirstOrDefault());
|
||||||
|
|
||||||
|
private async Task MoveCursorToLast()
|
||||||
|
=> await SelectNewSelectedItem(items => items.LastOrDefault());
|
||||||
|
|
||||||
|
private Task SelectNewSelectedItem(Func<IEnumerable<IItemViewModel>, IItemViewModel?> getNewSelected)
|
||||||
|
{
|
||||||
|
if (_selectedTab is null || _currentItems?.Value is null) return Task.CompletedTask;
|
||||||
|
|
||||||
|
var newSelectedItem = getNewSelected(_currentItems.Value);
|
||||||
|
if (newSelectedItem == null) return Task.CompletedTask;
|
||||||
|
|
||||||
|
if (_selectedTab.Tab is { } tab)
|
||||||
|
{
|
||||||
|
tab.SetSelectedItem(newSelectedItem.ToAbsolutePath(_timelessContentProvider));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task MoveCursorToFirst()
|
|
||||||
{
|
|
||||||
SelectNewSelectedItem(items => items.FirstOrDefault());
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task MoveCursorToLast()
|
|
||||||
{
|
|
||||||
SelectNewSelectedItem(items => items.LastOrDefault());
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SelectNewSelectedItem(Func<IEnumerable<IItemViewModel>, IItemViewModel?> getNewSelected)
|
|
||||||
{
|
|
||||||
if (_selectedTab is null || _currentLocation is null) return;
|
|
||||||
|
|
||||||
var newSelectedItem = getNewSelected(_currentItems);
|
|
||||||
if (newSelectedItem == null) return;
|
|
||||||
|
|
||||||
_selectedTab.Tab?.SetSelectedItem(newSelectedItem.ToAbsolutePath(_timelessContentProvider));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task EnterRapidTravel()
|
private Task EnterRapidTravel()
|
||||||
{
|
{
|
||||||
_appState.SwitchViewMode(ViewMode.RapidTravel);
|
_appState.SwitchViewMode(ViewMode.RapidTravel);
|
||||||
@@ -257,7 +272,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SwitchToTab(SwitchToTabCommand command)
|
private async Task SwitchToTab(SwitchToTabCommand command)
|
||||||
{
|
{
|
||||||
var number = command.TabNumber;
|
var number = command.TabNumber;
|
||||||
var tabViewModel = _appState.Tabs.FirstOrDefault(t => t.TabNumber == number);
|
var tabViewModel = _appState.Tabs.FirstOrDefault(t => t.TabNumber == number);
|
||||||
@@ -269,8 +284,8 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
|||||||
}
|
}
|
||||||
else if (tabViewModel == null)
|
else if (tabViewModel == null)
|
||||||
{
|
{
|
||||||
var tab = _serviceProvider.GetInitableResolver<IContainer>(_currentLocation ?? _localContentProvider)
|
var tab = await _serviceProvider.GetAsyncInitableResolver<IContainer>(_currentLocation?.Value ?? _localContentProvider)
|
||||||
.GetRequiredService<ITab>();
|
.GetRequiredServiceAsync<ITab>();
|
||||||
var newTabViewModel = _serviceProvider.GetInitableResolver(tab, number).GetRequiredService<ITabViewModel>();
|
var newTabViewModel = _serviceProvider.GetInitableResolver(tab, number).GetRequiredService<ITabViewModel>();
|
||||||
|
|
||||||
_appState.AddTab(newTabViewModel);
|
_appState.AddTab(newTabViewModel);
|
||||||
@@ -279,12 +294,10 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
|||||||
|
|
||||||
if (_viewMode == ViewMode.RapidTravel)
|
if (_viewMode == ViewMode.RapidTravel)
|
||||||
{
|
{
|
||||||
_userCommandHandlerService.HandleCommandAsync(ExitRapidTravelCommand.Instance);
|
await _userCommandHandlerService.HandleCommandAsync(ExitRapidTravelCommand.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
_appState.SetSelectedTab(tabViewModel!);
|
_appState.SetSelectedTab(tabViewModel!);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task CloseTab()
|
private Task CloseTab()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using DeclarativeProperty;
|
||||||
using FileTime.App.Core.UserCommand;
|
using FileTime.App.Core.UserCommand;
|
||||||
using FileTime.App.Core.ViewModels;
|
using FileTime.App.Core.ViewModels;
|
||||||
using FileTime.App.Search;
|
using FileTime.App.Search;
|
||||||
@@ -19,8 +20,8 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
|
|||||||
private readonly ITimelessContentProvider _timelessContentProvider;
|
private readonly ITimelessContentProvider _timelessContentProvider;
|
||||||
private readonly IUserCommandHandlerService _userCommandHandlerService;
|
private readonly IUserCommandHandlerService _userCommandHandlerService;
|
||||||
private readonly IContentAccessorFactory _contentAccessorFactory;
|
private readonly IContentAccessorFactory _contentAccessorFactory;
|
||||||
private IContainer? _currentLocation;
|
private IDeclarativeProperty<IContainer?>? _currentLocation;
|
||||||
private IItemViewModel? _currentSelectedItem;
|
private IDeclarativeProperty<IItemViewModel?>? _currentSelectedItem;
|
||||||
|
|
||||||
public ToolUserCommandHandlerService(
|
public ToolUserCommandHandlerService(
|
||||||
IAppState appState,
|
IAppState appState,
|
||||||
@@ -53,7 +54,7 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
|
|||||||
|
|
||||||
private async Task CopyBase64()
|
private async Task CopyBase64()
|
||||||
{
|
{
|
||||||
var item = _currentSelectedItem?.BaseItem;
|
var item = _currentSelectedItem?.Value?.BaseItem;
|
||||||
if (item?.Type != AbsolutePathType.Element || item is not IElement element) return;
|
if (item?.Type != AbsolutePathType.Element || item is not IElement element) return;
|
||||||
|
|
||||||
var contentReader = await _contentAccessorFactory.GetContentReaderFactory(element.Provider).CreateContentReaderAsync(element);
|
var contentReader = await _contentAccessorFactory.GetContentReaderFactory(element.Provider).CreateContentReaderAsync(element);
|
||||||
@@ -72,7 +73,7 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
|
|||||||
|
|
||||||
private async Task Search(SearchCommand searchCommand)
|
private async Task Search(SearchCommand searchCommand)
|
||||||
{
|
{
|
||||||
if (_currentLocation is null) return;
|
if (_currentLocation?.Value is null) return;
|
||||||
|
|
||||||
var searchQuery = searchCommand.SearchText;
|
var searchQuery = searchCommand.SearchText;
|
||||||
if (string.IsNullOrEmpty(searchQuery))
|
if (string.IsNullOrEmpty(searchQuery))
|
||||||
@@ -102,21 +103,21 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
|
|||||||
_ => throw new ArgumentOutOfRangeException()
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
};
|
};
|
||||||
|
|
||||||
var searchTask = await _searchManager.StartSearchAsync(searchMatcher, _currentLocation);
|
var searchTask = await _searchManager.StartSearchAsync(searchMatcher, _currentLocation.Value);
|
||||||
var openContainerCommand = new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, searchTask.SearchContainer));
|
var openContainerCommand = new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, searchTask.SearchContainer));
|
||||||
await _userCommandHandlerService.HandleCommandAsync(openContainerCommand);
|
await _userCommandHandlerService.HandleCommandAsync(openContainerCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CopyNativePath()
|
private async Task CopyNativePath()
|
||||||
{
|
{
|
||||||
if (_currentSelectedItem?.BaseItem?.NativePath is null) return;
|
if (_currentSelectedItem?.Value?.BaseItem?.NativePath is null) return;
|
||||||
await _systemClipboardService.CopyToClipboardAsync(_currentSelectedItem.BaseItem.NativePath.Path);
|
await _systemClipboardService.CopyToClipboardAsync(_currentSelectedItem.Value.BaseItem.NativePath.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task OpenInDefaultFileExplorer()
|
private Task OpenInDefaultFileExplorer()
|
||||||
{
|
{
|
||||||
if (_currentLocation?.NativePath is null) return Task.CompletedTask;
|
if (_currentLocation?.Value?.NativePath is null) return Task.CompletedTask;
|
||||||
Process.Start("explorer.exe", "\"" + _currentLocation.NativePath.Path + "\"");
|
Process.Start("explorer.exe", "\"" + _currentLocation.Value.NativePath.Path + "\"");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
|
using DeclarativeProperty;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using FileTime.App.Core.ViewModels;
|
using FileTime.App.Core.ViewModels;
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
@@ -44,14 +46,14 @@ public abstract class UserCommandHandlerServiceBase : IUserCommandHandler
|
|||||||
|
|
||||||
protected IDisposable SaveSelectedTab(Action<ITabViewModel?> handler) => RunWithAppState(appState => appState.SelectedTab.Subscribe(handler));
|
protected IDisposable SaveSelectedTab(Action<ITabViewModel?> handler) => RunWithAppState(appState => appState.SelectedTab.Subscribe(handler));
|
||||||
|
|
||||||
protected IDisposable SaveCurrentSelectedItem(Action<IItemViewModel?> handler)
|
protected IDisposable SaveCurrentSelectedItem(Action<IDeclarativeProperty<IItemViewModel?>?> handler)
|
||||||
=> RunWithAppState(appState => appState.SelectedTab.Select(t => t == null ? Observable.Return<IItemViewModel?>(null) : t.CurrentSelectedItem).Switch().Subscribe(handler));
|
=> RunWithAppState(appState => appState.SelectedTab.Select(t => t?.CurrentSelectedItem).Subscribe(handler));
|
||||||
|
|
||||||
protected IDisposable SaveCurrentLocation(Action<IContainer?> handler)
|
protected IDisposable SaveCurrentLocation(Action<IDeclarativeProperty<IContainer?>?> handler)
|
||||||
=> RunWithAppState(appState => appState.SelectedTab.Select(t => t == null ? Observable.Return<IContainer?>(null) : t.CurrentLocation).Switch().Subscribe(handler));
|
=> RunWithAppState(appState => appState.SelectedTab.Select(t => t?.CurrentLocation).Subscribe(handler));
|
||||||
|
|
||||||
protected IDisposable SaveCurrentItems(Action<IEnumerable<IItemViewModel>> handler)
|
protected IDisposable SaveCurrentItems(Action<IDeclarativeProperty<ObservableCollection<IItemViewModel>?>?> handler)
|
||||||
=> RunWithAppState(appState => appState.SelectedTab.Select(t => t?.CurrentItemsCollectionObservable ?? Observable.Return((IEnumerable<IItemViewModel>?) Enumerable.Empty<IItemViewModel>())).Switch().Subscribe(i => handler(i ?? Enumerable.Empty<IItemViewModel>())));
|
=> RunWithAppState(appState => appState.SelectedTab.Select(t => t?.CurrentItems).Subscribe(handler));
|
||||||
|
|
||||||
protected IDisposable SaveMarkedItems(Action<IChangeSet<FullName>> handler)
|
protected IDisposable SaveMarkedItems(Action<IChangeSet<FullName>> handler)
|
||||||
=> RunWithAppState(appState => appState.SelectedTab.Select(t => t == null ? Observable.Empty<IChangeSet<FullName>>() : t.MarkedItems).Switch().Subscribe(handler));
|
=> RunWithAppState(appState => appState.SelectedTab.Select(t => t == null ? Observable.Empty<IChangeSet<FullName>>() : t.MarkedItems).Switch().Subscribe(handler));
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public static class Startup
|
|||||||
serviceCollection.TryAddSingleton<IIdentifiableUserCommandService, IdentifiableUserCommandService>();
|
serviceCollection.TryAddSingleton<IIdentifiableUserCommandService, IdentifiableUserCommandService>();
|
||||||
serviceCollection.TryAddSingleton<IItemPreviewService, ItemPreviewService>();
|
serviceCollection.TryAddSingleton<IItemPreviewService, ItemPreviewService>();
|
||||||
serviceCollection.TryAddSingleton<ITimelineViewModel, TimelineViewModel>();
|
serviceCollection.TryAddSingleton<ITimelineViewModel, TimelineViewModel>();
|
||||||
|
serviceCollection.TryAddSingleton<IRefreshSmoothnessCalculator, RefreshSmoothnessCalculator>();
|
||||||
|
|
||||||
return serviceCollection
|
return serviceCollection
|
||||||
.AddCommandHandlers()
|
.AddCommandHandlers()
|
||||||
|
|||||||
@@ -10,12 +10,11 @@ public partial class ContainerViewModel : ItemViewModel, IContainerViewModel
|
|||||||
{
|
{
|
||||||
public IContainer? Container => BaseItem as IContainer;
|
public IContainer? Container => BaseItem as IContainer;
|
||||||
|
|
||||||
public ContainerViewModel(IItemNameConverterService itemNameConverterService, IAppState appState) : base(itemNameConverterService, appState)
|
public ContainerViewModel(IItemNameConverterService itemNameConverterService, IAppState appState)
|
||||||
|
: base(itemNameConverterService, appState)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Init(IContainer item, ITabViewModel parentTab, ItemViewModelType itemViewModelType)
|
public void Init(IContainer item, ITabViewModel parentTab, ItemViewModelType itemViewModelType)
|
||||||
{
|
=> Init((IItem)item, parentTab, itemViewModelType);
|
||||||
Init((IItem)item, parentTab, itemViewModelType);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
|
using DeclarativeProperty;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using FileTime.App.Core.Models;
|
using FileTime.App.Core.Models;
|
||||||
using FileTime.App.Core.Models.Enums;
|
using FileTime.App.Core.Models.Enums;
|
||||||
@@ -18,32 +19,23 @@ public abstract partial class ItemViewModel : IItemViewModel
|
|||||||
{
|
{
|
||||||
private ITabViewModel? _parentTab;
|
private ITabViewModel? _parentTab;
|
||||||
|
|
||||||
[Property]
|
[Property] private IItem? _baseItem;
|
||||||
private IItem? _baseItem;
|
|
||||||
|
|
||||||
[Property]
|
[Property] private IObservable<IReadOnlyList<ItemNamePart>>? _displayName;
|
||||||
private IObservable<IReadOnlyList<ItemNamePart>>? _displayName;
|
|
||||||
|
|
||||||
[Property]
|
[Property] private string? _displayNameText;
|
||||||
private string? _displayNameText;
|
|
||||||
|
|
||||||
[Property]
|
[Property] private IDeclarativeProperty<bool> _isSelected;
|
||||||
private IObservable<bool>? _isSelected;
|
|
||||||
|
|
||||||
[Property]
|
[Property] private IObservable<bool>? _isMarked;
|
||||||
private IObservable<bool>? _isMarked;
|
|
||||||
|
|
||||||
[Property]
|
[Property] private IObservable<ItemViewMode> _viewMode;
|
||||||
private IObservable<ItemViewMode> _viewMode;
|
|
||||||
|
|
||||||
[Property]
|
[Property] private DateTime? _createdAt;
|
||||||
private DateTime? _createdAt;
|
|
||||||
|
|
||||||
[Property]
|
[Property] private string? _attributes;
|
||||||
private string? _attributes;
|
|
||||||
|
|
||||||
[Property]
|
[Property] private IDeclarativeProperty<bool> _isAlternative;
|
||||||
private IObservable<bool> _isAlternative;
|
|
||||||
|
|
||||||
public void Init(IItem item, ITabViewModel parentTab, ItemViewModelType itemViewModelType)
|
public void Init(IItem item, ITabViewModel parentTab, ItemViewModelType itemViewModelType)
|
||||||
{
|
{
|
||||||
@@ -51,9 +43,9 @@ public abstract partial class ItemViewModel : IItemViewModel
|
|||||||
|
|
||||||
var sourceCollection = itemViewModelType switch
|
var sourceCollection = itemViewModelType switch
|
||||||
{
|
{
|
||||||
ItemViewModelType.Main => parentTab.CurrentItemsCollectionObservable,
|
ItemViewModelType.Main => parentTab.CurrentItems,
|
||||||
ItemViewModelType.Parent => parentTab.ParentsChildrenCollectionObservable,
|
ItemViewModelType.Parent => parentTab.ParentsChildren,
|
||||||
ItemViewModelType.SelectedChild => parentTab.SelectedsChildrenCollectionObservable,
|
ItemViewModelType.SelectedChild => parentTab.SelectedsChildren,
|
||||||
_ => throw new InvalidEnumArgumentException()
|
_ => throw new InvalidEnumArgumentException()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,10 +58,10 @@ public abstract partial class ItemViewModel : IItemViewModel
|
|||||||
: Observable.Return(false);
|
: Observable.Return(false);
|
||||||
|
|
||||||
IsSelected = itemViewModelType is ItemViewModelType.Main
|
IsSelected = itemViewModelType is ItemViewModelType.Main
|
||||||
? parentTab.CurrentSelectedItem.Select(EqualsTo)
|
? parentTab.CurrentSelectedItem.Map(EqualsTo)
|
||||||
: Observable.Return(IsInDeepestPath());
|
: new DeclarativeProperty<bool>(IsInDeepestPath());
|
||||||
|
|
||||||
IsAlternative = sourceCollection.Select(c => c?.Index().FirstOrDefault(i => EqualsTo(i.Value)).Key % 2 == 0);
|
IsAlternative = sourceCollection.Map(c => c?.Index().FirstOrDefault(i => EqualsTo(i.Value)).Key % 2 == 0);
|
||||||
|
|
||||||
ViewMode = Observable.CombineLatest(IsMarked, IsSelected, IsAlternative, GenerateViewMode).Throttle(TimeSpan.FromMilliseconds(10));
|
ViewMode = Observable.CombineLatest(IsMarked, IsSelected, IsAlternative, GenerateViewMode).Throttle(TimeSpan.FromMilliseconds(10));
|
||||||
Attributes = item.Attributes;
|
Attributes = item.Attributes;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
|
using DeclarativeProperty;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using DynamicData.Binding;
|
using DynamicData.Binding;
|
||||||
using FileTime.App.Core.Extensions;
|
using FileTime.App.Core.Extensions;
|
||||||
@@ -10,6 +13,9 @@ using FileTime.Core.Models.Extensions;
|
|||||||
using FileTime.Core.Services;
|
using FileTime.Core.Services;
|
||||||
using InitableService;
|
using InitableService;
|
||||||
using MvvmGen;
|
using MvvmGen;
|
||||||
|
using ObservableComputations;
|
||||||
|
using IContainer = FileTime.Core.Models.IContainer;
|
||||||
|
using static System.DeferTools;
|
||||||
|
|
||||||
namespace FileTime.App.Core.ViewModels;
|
namespace FileTime.App.Core.ViewModels;
|
||||||
|
|
||||||
@@ -23,36 +29,23 @@ public partial class TabViewModel : ITabViewModel
|
|||||||
private readonly SourceList<FullName> _markedItems = new();
|
private readonly SourceList<FullName> _markedItems = new();
|
||||||
private readonly List<IDisposable> _disposables = new();
|
private readonly List<IDisposable> _disposables = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
private OcConsumer? _currentItemsConsumer;
|
||||||
|
private OcConsumer? _selectedsChildrenConsumer;
|
||||||
|
private OcConsumer? _parentsChildrenConsumer;
|
||||||
|
|
||||||
public ITab? Tab { get; private set; }
|
public ITab? Tab { get; private set; }
|
||||||
public int TabNumber { get; private set; }
|
public int TabNumber { get; private set; }
|
||||||
|
|
||||||
public IObservable<bool> IsSelected { get; }
|
public IObservable<bool> IsSelected { get; }
|
||||||
|
|
||||||
public IObservable<IContainer?> CurrentLocation { get; private set; } = null!;
|
public IDeclarativeProperty<IContainer?> CurrentLocation { get; private set; }
|
||||||
public IObservable<IItemViewModel?> CurrentSelectedItem { get; private set; } = null!;
|
public IDeclarativeProperty<IItemViewModel?> CurrentSelectedItem { get; private set; }
|
||||||
public IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> CurrentItems { get; private set; } = null!;
|
public IDeclarativeProperty<IContainerViewModel?> CurrentSelectedItemAsContainer { get; private set; }
|
||||||
|
public IDeclarativeProperty<ObservableCollection<IItemViewModel>?> CurrentItems { get; private set; }
|
||||||
public IObservable<IChangeSet<FullName>> MarkedItems { get; }
|
public IObservable<IChangeSet<FullName>> MarkedItems { get; }
|
||||||
public IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> SelectedsChildren { get; private set; } = null!;
|
public IDeclarativeProperty<ObservableCollection<IItemViewModel>?> SelectedsChildren { get; private set; }
|
||||||
public IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> ParentsChildren { get; private set; } = null!;
|
public IDeclarativeProperty<ObservableCollection<IItemViewModel>?> ParentsChildren { get; private set; }
|
||||||
|
|
||||||
public IObservable<IReadOnlyCollection<IItemViewModel>?> CurrentItemsCollectionObservable { get; private set; } =
|
|
||||||
null!;
|
|
||||||
|
|
||||||
public IObservable<IReadOnlyCollection<IItemViewModel>?> ParentsChildrenCollectionObservable { get; private set; } =
|
|
||||||
null!;
|
|
||||||
|
|
||||||
public IObservable<IReadOnlyCollection<IItemViewModel>?>
|
|
||||||
SelectedsChildrenCollectionObservable
|
|
||||||
{ get; private set; } = null!;
|
|
||||||
|
|
||||||
[Property] private BindedCollection<IItemViewModel, string>? _currentItemsCollection;
|
|
||||||
|
|
||||||
[Property] private BindedCollection<IItemViewModel, string>? _parentsChildrenCollection;
|
|
||||||
|
|
||||||
[Property] private BindedCollection<IItemViewModel, string>? _selectedsChildrenCollection;
|
|
||||||
|
|
||||||
public IContainer? CachedCurrentLocation { get; private set; }
|
|
||||||
|
|
||||||
public TabViewModel(
|
public TabViewModel(
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
@@ -76,13 +69,19 @@ public partial class TabViewModel : ITabViewModel
|
|||||||
|
|
||||||
tab.AddToDisposables(_disposables);
|
tab.AddToDisposables(_disposables);
|
||||||
|
|
||||||
CurrentLocation = tab.CurrentLocation.AsObservable();
|
CurrentLocation = tab.CurrentLocation;
|
||||||
CurrentLocation.Subscribe(l => CachedCurrentLocation = l).AddToDisposables(_disposables);
|
|
||||||
|
|
||||||
CurrentItems = tab.CurrentItems
|
CurrentItems = tab.CurrentItems
|
||||||
.Select(items => items?.Transform(i => MapItemToViewModel(i, ItemViewModelType.Main)))
|
.Map((items, _) =>
|
||||||
.Publish(null)
|
Task.FromResult<ObservableCollection<IItemViewModel>?>(
|
||||||
.RefCount();
|
items?.Selecting<IItem, IItemViewModel>(
|
||||||
|
i => MapItemToViewModel(i, ItemViewModelType.Main)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
using var _ = Defer(
|
||||||
|
() => CurrentItems.Subscribe(c => UpdateConsumer(c, ref _currentItemsConsumer))
|
||||||
|
);
|
||||||
|
|
||||||
/*CurrentSelectedItem =
|
/*CurrentSelectedItem =
|
||||||
Observable.CombineLatest(
|
Observable.CombineLatest(
|
||||||
@@ -100,7 +99,17 @@ public partial class TabViewModel : ITabViewModel
|
|||||||
.Publish(null)
|
.Publish(null)
|
||||||
.RefCount();*/
|
.RefCount();*/
|
||||||
|
|
||||||
CurrentSelectedItem =
|
CurrentSelectedItem = DeclarativePropertyHelpers.CombineLatest(
|
||||||
|
tab.CurrentSelectedItem,
|
||||||
|
CurrentItems.Watch<ObservableCollection<IItemViewModel>, IItemViewModel>(),
|
||||||
|
(currentSelectedItem, currentItems) =>
|
||||||
|
Task.FromResult(currentItems?.FirstOrDefault(i => i.BaseItem?.FullName?.Path == currentSelectedItem?.Path.Path))
|
||||||
|
);
|
||||||
|
|
||||||
|
CurrentSelectedItemAsContainer = CurrentSelectedItem.Map(i => i as IContainerViewModel);
|
||||||
|
//CurrentSelectedItem = tab.CurrentSelectedItem.Map((item, _) => Task.FromResult(CurrentItems.Value?.FirstOrDefault(i => i.BaseItem?.FullName?.Path == item?.Path.Path)));
|
||||||
|
|
||||||
|
/*CurrentSelectedItem =
|
||||||
Observable.CombineLatest(
|
Observable.CombineLatest(
|
||||||
CurrentItems,
|
CurrentItems,
|
||||||
tab.CurrentSelectedItem,
|
tab.CurrentSelectedItem,
|
||||||
@@ -108,22 +117,47 @@ public partial class TabViewModel : ITabViewModel
|
|||||||
CurrentItemsCollection?.Collection?.FirstOrDefault(i => i.BaseItem?.FullName?.Path == currentSelectedItemPath?.Path.Path)
|
CurrentItemsCollection?.Collection?.FirstOrDefault(i => i.BaseItem?.FullName?.Path == currentSelectedItemPath?.Path.Path)
|
||||||
)
|
)
|
||||||
.Publish(null)
|
.Publish(null)
|
||||||
.RefCount();
|
.RefCount();*/
|
||||||
|
|
||||||
SelectedsChildren = InitSelectedsChildren();
|
SelectedsChildren = CurrentSelectedItem
|
||||||
ParentsChildren = InitParentsChildren();
|
.Debounce(TimeSpan.FromMilliseconds(200), resetTimer: true)
|
||||||
|
.DistinctUntilChanged()
|
||||||
|
.Map(item =>
|
||||||
|
{
|
||||||
|
if (item is not IContainerViewModel {Container: { } container})
|
||||||
|
return (ObservableCollection<IItemViewModel>?) null;
|
||||||
|
|
||||||
CurrentItemsCollectionObservable = InitCollection(CurrentItems);
|
var items = container
|
||||||
SelectedsChildrenCollectionObservable = InitCollection(SelectedsChildren);
|
.Items
|
||||||
ParentsChildrenCollectionObservable = InitCollection(ParentsChildren);
|
.Selecting(i => MapItem(i))
|
||||||
|
.Ordering(i => i.Type)
|
||||||
|
.ThenOrdering(i => i.Name)
|
||||||
|
.Selecting(i => MapItemToViewModel(i, ItemViewModelType.SelectedChild));
|
||||||
|
|
||||||
CurrentItemsCollection = new(CurrentItems);
|
return items;
|
||||||
ParentsChildrenCollection = new(ParentsChildren);
|
});
|
||||||
SelectedsChildrenCollection = new(SelectedsChildren);
|
using var __ = Defer(() =>
|
||||||
|
SelectedsChildren.Subscribe(c => UpdateConsumer(c, ref _selectedsChildrenConsumer))
|
||||||
|
);
|
||||||
|
|
||||||
tab.CurrentLocation.Subscribe((_) => _markedItems.Clear()).AddToDisposables(_disposables);
|
ParentsChildren = CurrentLocation.Map(async (item, _) =>
|
||||||
|
{
|
||||||
|
if (item is null || item.Parent is null) return (ObservableCollection<IItemViewModel>?) null;
|
||||||
|
var parent = (IContainer) await item.Parent.ResolveAsync(itemInitializationSettings: new ItemInitializationSettings(true));
|
||||||
|
|
||||||
IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> InitSelectedsChildren()
|
var items = parent.Items
|
||||||
|
.Selecting<AbsolutePath, IItem>(i => MapItem(i))
|
||||||
|
.Selecting(i => MapItemToViewModel(i, ItemViewModelType.SelectedChild));
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
using var ___ = Defer(() =>
|
||||||
|
ParentsChildren.Subscribe(c => UpdateConsumer(c, ref _parentsChildrenConsumer))
|
||||||
|
);
|
||||||
|
|
||||||
|
tab.CurrentLocation.Subscribe(_ => _markedItems.Clear()).AddToDisposables(_disposables);
|
||||||
|
|
||||||
|
/*IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> InitSelectedsChildren()
|
||||||
{
|
{
|
||||||
var currentSelectedItemThrottled =
|
var currentSelectedItemThrottled =
|
||||||
CurrentSelectedItem.Throttle(TimeSpan.FromMilliseconds(250)).Publish(null).RefCount();
|
CurrentSelectedItem.Throttle(TimeSpan.FromMilliseconds(250)).Publish(null).RefCount();
|
||||||
@@ -135,7 +169,7 @@ public partial class TabViewModel : ITabViewModel
|
|||||||
.Select(c => c.Container!.Items)
|
.Select(c => c.Container!.Items)
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
i
|
i
|
||||||
?.TransformAsync(MapItem)
|
?.TransformAsync(MapItemAsync)
|
||||||
.Transform(i => MapItemToViewModel(i, ItemViewModelType.SelectedChild))
|
.Transform(i => MapItemToViewModel(i, ItemViewModelType.SelectedChild))
|
||||||
.Sort(SortItems())
|
.Sort(SortItems())
|
||||||
),
|
),
|
||||||
@@ -144,7 +178,7 @@ public partial class TabViewModel : ITabViewModel
|
|||||||
.Select(_ => (IObservable<IChangeSet<IItemViewModel, string>>?) null)
|
.Select(_ => (IObservable<IChangeSet<IItemViewModel, string>>?) null)
|
||||||
)
|
)
|
||||||
/*.ObserveOn(_rxSchedulerService.GetWorkerScheduler())
|
/*.ObserveOn(_rxSchedulerService.GetWorkerScheduler())
|
||||||
.SubscribeOn(_rxSchedulerService.GetUIScheduler())*/
|
.SubscribeOn(_rxSchedulerService.GetUIScheduler())#1#
|
||||||
.Publish(null)
|
.Publish(null)
|
||||||
.RefCount();
|
.RefCount();
|
||||||
}
|
}
|
||||||
@@ -165,7 +199,7 @@ public partial class TabViewModel : ITabViewModel
|
|||||||
.Select(p => p.Items)
|
.Select(p => p.Items)
|
||||||
.Select(items =>
|
.Select(items =>
|
||||||
items
|
items
|
||||||
?.TransformAsync(MapItem)
|
?.TransformAsync(MapItemAsync)
|
||||||
.Transform(i => MapItemToViewModel(i, ItemViewModelType.Parent))
|
.Transform(i => MapItemToViewModel(i, ItemViewModelType.Parent))
|
||||||
.Sort(SortItems())
|
.Sort(SortItems())
|
||||||
),
|
),
|
||||||
@@ -174,21 +208,20 @@ public partial class TabViewModel : ITabViewModel
|
|||||||
.Select(_ => (IObservable<IChangeSet<IItemViewModel, string>>?) null)
|
.Select(_ => (IObservable<IChangeSet<IItemViewModel, string>>?) null)
|
||||||
)
|
)
|
||||||
/*.ObserveOn(_rxSchedulerService.GetWorkerScheduler())
|
/*.ObserveOn(_rxSchedulerService.GetWorkerScheduler())
|
||||||
.SubscribeOn(_rxSchedulerService.GetUIScheduler())*/
|
.SubscribeOn(_rxSchedulerService.GetUIScheduler())#1#
|
||||||
.Publish(null)
|
.Publish(null)
|
||||||
.RefCount();
|
.RefCount();
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
IObservable<IReadOnlyCollection<IItemViewModel>?> InitCollection(
|
|
||||||
IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> source)
|
static void UpdateConsumer<T>(ObservableCollection<T>? collection, ref OcConsumer? consumer)
|
||||||
{
|
{
|
||||||
return source
|
if (collection is not IComputing computing) return;
|
||||||
.Select(c =>
|
|
||||||
c != null ? c.ToCollection() : Observable.Return((IReadOnlyCollection<IItemViewModel>?)null))
|
consumer?.Dispose();
|
||||||
.Switch()
|
consumer = new OcConsumer();
|
||||||
.Publish(null)
|
computing.For(consumer);
|
||||||
.RefCount();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SortExpressionComparer<IItemViewModel> SortItems()
|
private static SortExpressionComparer<IItemViewModel> SortItems()
|
||||||
@@ -197,7 +230,14 @@ public partial class TabViewModel : ITabViewModel
|
|||||||
.Ascending(i => i.BaseItem?.Type ?? AbsolutePathType.Unknown)
|
.Ascending(i => i.BaseItem?.Type ?? AbsolutePathType.Unknown)
|
||||||
.ThenByAscending(i => i.DisplayNameText?.ToLower() ?? "");
|
.ThenByAscending(i => i.DisplayNameText?.ToLower() ?? "");
|
||||||
|
|
||||||
private static async Task<IItem> MapItem(AbsolutePath item)
|
private static IItem MapItem(AbsolutePath item)
|
||||||
|
{
|
||||||
|
var t = Task.Run(async () => await MapItemAsync(item));
|
||||||
|
t.Wait();
|
||||||
|
return t.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IItem> MapItemAsync(AbsolutePath item)
|
||||||
=> await item.ResolveAsync(forceResolve: true,
|
=> await item.ResolveAsync(forceResolve: true,
|
||||||
itemInitializationSettings: new ItemInitializationSettings(true));
|
itemInitializationSettings: new ItemInitializationSettings(true));
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using FileTime.Core.ContentAccess;
|
using FileTime.Core.ContentAccess;
|
||||||
using FileTime.Core.Enums;
|
using FileTime.Core.Enums;
|
||||||
@@ -11,8 +12,8 @@ public class SearchTask : ISearchTask
|
|||||||
private readonly IContainer _baseContainer;
|
private readonly IContainer _baseContainer;
|
||||||
private readonly ISearchMatcher _matcher;
|
private readonly ISearchMatcher _matcher;
|
||||||
private readonly Container _container;
|
private readonly Container _container;
|
||||||
private readonly SourceList<Exception> _exceptions = new();
|
private readonly ObservableCollection<Exception> _exceptions = new();
|
||||||
private readonly SourceCache<AbsolutePath, string> _items = new(p => p.Path.Path);
|
private readonly ObservableCollection<AbsolutePath> _items = new();
|
||||||
private readonly SemaphoreSlim _searchingLock = new(1, 1);
|
private readonly SemaphoreSlim _searchingLock = new(1, 1);
|
||||||
private bool _isSearching;
|
private bool _isSearching;
|
||||||
private static int _searchId = 1;
|
private static int _searchId = 1;
|
||||||
@@ -43,9 +44,9 @@ public class SearchTask : ISearchTask
|
|||||||
contentProvider,
|
contentProvider,
|
||||||
false,
|
false,
|
||||||
PointInTime.Present,
|
PointInTime.Present,
|
||||||
_exceptions.Connect(),
|
_exceptions,
|
||||||
new ReadOnlyExtensionCollection(new ExtensionCollection()),
|
new ReadOnlyExtensionCollection(new ExtensionCollection()),
|
||||||
_items.Connect().StartWithEmpty()
|
_items
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ public class SearchTask : ISearchTask
|
|||||||
|
|
||||||
private async Task TraverseTree(IContainer container)
|
private async Task TraverseTree(IContainer container)
|
||||||
{
|
{
|
||||||
var items = container.ItemsCollection.ToList();
|
var items = container.Items.ToList();
|
||||||
|
|
||||||
var childContainers = new List<IContainer>();
|
var childContainers = new List<IContainer>();
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ public class SearchTask : ISearchTask
|
|||||||
var item = await itemPath.ResolveAsync();
|
var item = await itemPath.ResolveAsync();
|
||||||
if (await _matcher.IsItemMatchAsync(item))
|
if (await _matcher.IsItemMatchAsync(item))
|
||||||
{
|
{
|
||||||
_items.AddOrUpdate(itemPath);
|
_items.Add(itemPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is IContainer childContainer)
|
if (item is IContainer childContainer)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Library\DeclarativeProperty\DeclarativeProperty.csproj" />
|
||||||
<ProjectReference Include="..\..\Library\InitableService\InitableService.csproj" />
|
<ProjectReference Include="..\..\Library\InitableService\InitableService.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ namespace FileTime.Core.Models;
|
|||||||
|
|
||||||
public interface IContainer : IItem
|
public interface IContainer : IItem
|
||||||
{
|
{
|
||||||
IObservable<IChangeSet<AbsolutePath, string>> Items { get; }
|
ObservableCollection<AbsolutePath> Items { get; }
|
||||||
ReadOnlyObservableCollection<AbsolutePath> ItemsCollection { get; }
|
|
||||||
IObservable<bool> IsLoading { get; }
|
IObservable<bool> IsLoading { get; }
|
||||||
bool? IsLoaded { get; }
|
bool? IsLoaded { get; }
|
||||||
Task WaitForLoaded(CancellationToken token = default);
|
Task WaitForLoaded(CancellationToken token = default);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using FileTime.Core.ContentAccess;
|
using FileTime.Core.ContentAccess;
|
||||||
using FileTime.Core.Enums;
|
using FileTime.Core.Enums;
|
||||||
@@ -21,7 +22,7 @@ public interface IItem
|
|||||||
string? Attributes { get; }
|
string? Attributes { get; }
|
||||||
AbsolutePathType Type { get; }
|
AbsolutePathType Type { get; }
|
||||||
PointInTime PointInTime { get; }
|
PointInTime PointInTime { get; }
|
||||||
IObservable<IChangeSet<Exception>> Exceptions { get; }
|
ObservableCollection<Exception> Exceptions { get; }
|
||||||
ReadOnlyExtensionCollection Extensions { get; }
|
ReadOnlyExtensionCollection Extensions { get; }
|
||||||
|
|
||||||
T? GetExtension<T>() => (T?)Extensions.FirstOrDefault(i => i is T);
|
T? GetExtension<T>() => (T?)Extensions.FirstOrDefault(i => i is T);
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using DeclarativeProperty;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
using InitableService;
|
using InitableService;
|
||||||
|
|
||||||
namespace FileTime.Core.Services;
|
namespace FileTime.Core.Services;
|
||||||
|
|
||||||
public interface ITab : IInitable<IContainer>, IDisposable
|
public interface ITab : IAsyncInitable<IContainer>, IDisposable
|
||||||
{
|
{
|
||||||
IObservable<IContainer?> CurrentLocation { get; }
|
public IDeclarativeProperty<IContainer?> CurrentLocation { get; }
|
||||||
IObservable<AbsolutePath?> CurrentSelectedItem { get; }
|
public IDeclarativeProperty<ObservableCollection<IItem>?> CurrentItems { get; }
|
||||||
IObservable<IObservable<IChangeSet<IItem, string>>?> CurrentItems { get; }
|
public IDeclarativeProperty<AbsolutePath?> CurrentSelectedItem { get; }
|
||||||
FullName? LastDeepestSelectedPath { get; }
|
FullName? LastDeepestSelectedPath { get; }
|
||||||
|
|
||||||
void SetCurrentLocation(IContainer newLocation);
|
Task SetCurrentLocation(IContainer newLocation);
|
||||||
void AddItemFilter(ItemFilter filter);
|
void AddItemFilter(ItemFilter filter);
|
||||||
void RemoveItemFilter(ItemFilter filter);
|
void RemoveItemFilter(ItemFilter filter);
|
||||||
void RemoveItemFilter(string name);
|
void RemoveItemFilter(string name);
|
||||||
void SetSelectedItem(AbsolutePath newSelectedItem);
|
Task SetSelectedItem(AbsolutePath newSelectedItem);
|
||||||
void ForceSetCurrentLocation(IContainer newLocation);
|
Task ForceSetCurrentLocation(IContainer newLocation);
|
||||||
}
|
}
|
||||||
@@ -140,12 +140,12 @@ public class CopyCommand : CommandBase, ITransportationCommand
|
|||||||
|
|
||||||
if (item is IContainer container)
|
if (item is IContainer container)
|
||||||
{
|
{
|
||||||
if (resolvedTarget.ItemsCollection.All(i => i.Path.GetName() != item.Name))
|
if (resolvedTarget.Items.All(i => i.Path.GetName() != item.Name))
|
||||||
{
|
{
|
||||||
await copyOperation.CreateContainerAsync(resolvedTarget, container.Name, container.PointInTime);
|
await copyOperation.CreateContainerAsync(resolvedTarget, container.Name, container.PointInTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
var children = container.ItemsCollection;
|
var children = container.Items;
|
||||||
|
|
||||||
await TraverseTree(currentTime, children.Select(c => c.Path).ToList(), target.GetChild(item.Name, AbsolutePathType.Container), transportMode, copyOperation);
|
await TraverseTree(currentTime, children.Select(c => c.Path).ToList(), target.GetChild(item.Name, AbsolutePathType.Container), transportMode, copyOperation);
|
||||||
await copyOperation.ContainerCopyDoneAsync(new AbsolutePath(_timelessContentProvider, container));
|
await copyOperation.ContainerCopyDoneAsync(new AbsolutePath(_timelessContentProvider, container));
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public abstract class CreateItemBase : CommandBase, IExecutableCommand, IInitabl
|
|||||||
var parent = await ResolveParentAsync();
|
var parent = await ResolveParentAsync();
|
||||||
if (parent is not IContainer parentContainer) return CanCommandRun.False;
|
if (parent is not IContainer parentContainer) return CanCommandRun.False;
|
||||||
|
|
||||||
var items = parentContainer.ItemsCollection;
|
var items = parentContainer.Items;
|
||||||
var existingItem = items.FirstOrDefault(i => i.Path.GetName() == NewItemName);
|
var existingItem = items.FirstOrDefault(i => i.Path.GetName() == NewItemName);
|
||||||
|
|
||||||
return existingItem switch
|
return existingItem switch
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ public class DeleteCommand : CommandBase, IExecutableCommand
|
|||||||
{
|
{
|
||||||
await TraverseTree(
|
await TraverseTree(
|
||||||
currentTime,
|
currentTime,
|
||||||
container.ItemsCollection.Select(i => i.Path),
|
container.Items.Select(i => i.Path),
|
||||||
itemDeleters,
|
itemDeleters,
|
||||||
deleteStrategy
|
deleteStrategy
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public static class Helper
|
|||||||
public static async Task<string?> GetNewNameAsync(IContainer resolvedTarget, string name, TransportMode transportMode)
|
public static async Task<string?> GetNewNameAsync(IContainer resolvedTarget, string name, TransportMode transportMode)
|
||||||
{
|
{
|
||||||
await resolvedTarget.WaitForLoaded();
|
await resolvedTarget.WaitForLoaded();
|
||||||
var items = resolvedTarget.ItemsCollection.ToList();
|
var items = resolvedTarget.Items.ToList();
|
||||||
var newName = name;
|
var newName = name;
|
||||||
var targetNameExists = items.Any(i => i.Path.GetName() == newName);
|
var targetNameExists = items.Any(i => i.Path.GetName() == newName);
|
||||||
if (transportMode == TransportMode.Merge)
|
if (transportMode == TransportMode.Merge)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class StreamCopyCommandHandler : ICommandHandler
|
|||||||
{
|
{
|
||||||
var parent = (IContainer?) (await targetPath.GetParent()!.ResolveAsync())!;
|
var parent = (IContainer?) (await targetPath.GetParent()!.ResolveAsync())!;
|
||||||
var elementName = targetPath.Path;
|
var elementName = targetPath.Path;
|
||||||
var parentChildren = parent.ItemsCollection.ToList();
|
var parentChildren = parent.Items.ToList();
|
||||||
if (parentChildren.All(e => e.Path.GetName() != elementName.GetName()))
|
if (parentChildren.All(e => e.Path.GetName() != elementName.GetName()))
|
||||||
{
|
{
|
||||||
var itemCreator = _contentAccessorFactory.GetItemCreator(parent.Provider);
|
var itemCreator = _contentAccessorFactory.GetItemCreator(parent.Provider);
|
||||||
|
|||||||
@@ -11,14 +11,9 @@ namespace FileTime.Core.ContentAccess;
|
|||||||
public abstract class ContentProviderBase : IContentProvider
|
public abstract class ContentProviderBase : IContentProvider
|
||||||
{
|
{
|
||||||
private readonly ReadOnlyExtensionCollection _extensions;
|
private readonly ReadOnlyExtensionCollection _extensions;
|
||||||
private readonly IObservable<IChangeSet<AbsolutePath, string>> _items;
|
public ObservableCollection<AbsolutePath> Items { get; }
|
||||||
|
|
||||||
protected SourceCache<AbsolutePath, string> Items { get; } = new(p => p.Path.Path);
|
|
||||||
public ReadOnlyObservableCollection<AbsolutePath> ItemsCollection { get; }
|
|
||||||
protected ExtensionCollection Extensions { get; }
|
protected ExtensionCollection Extensions { get; }
|
||||||
|
|
||||||
IObservable<IChangeSet<AbsolutePath, string>> IContainer.Items => _items;
|
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public string DisplayName { get; }
|
public string DisplayName { get; }
|
||||||
@@ -53,8 +48,7 @@ public abstract class ContentProviderBase : IContentProvider
|
|||||||
public AbsolutePathType Type => AbsolutePathType.Container;
|
public AbsolutePathType Type => AbsolutePathType.Container;
|
||||||
public PointInTime PointInTime { get; } = PointInTime.Eternal;
|
public PointInTime PointInTime { get; } = PointInTime.Eternal;
|
||||||
|
|
||||||
protected SourceList<Exception> Exceptions { get; } = new();
|
public ObservableCollection<Exception> Exceptions { get; } = new();
|
||||||
IObservable<IChangeSet<Exception>> IItem.Exceptions => Exceptions.Connect();
|
|
||||||
|
|
||||||
ReadOnlyExtensionCollection IItem.Extensions => _extensions;
|
ReadOnlyExtensionCollection IItem.Extensions => _extensions;
|
||||||
|
|
||||||
@@ -64,9 +58,8 @@ public abstract class ContentProviderBase : IContentProvider
|
|||||||
FullName = FullName.CreateSafe(name);
|
FullName = FullName.CreateSafe(name);
|
||||||
Extensions = new ExtensionCollection();
|
Extensions = new ExtensionCollection();
|
||||||
_extensions = Extensions.AsReadOnly();
|
_extensions = Extensions.AsReadOnly();
|
||||||
_items = Items.Connect().StartWithEmpty();
|
//TODO:
|
||||||
_items.Bind(out var items).Subscribe();
|
Items = new ObservableCollection<AbsolutePath>();
|
||||||
ItemsCollection = items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual Task OnEnter() => Task.CompletedTask;
|
public virtual Task OnEnter() => Task.CompletedTask;
|
||||||
|
|||||||
@@ -23,17 +23,10 @@ public record Container(
|
|||||||
IContentProvider Provider,
|
IContentProvider Provider,
|
||||||
bool AllowRecursiveDeletion,
|
bool AllowRecursiveDeletion,
|
||||||
PointInTime PointInTime,
|
PointInTime PointInTime,
|
||||||
IObservable<IChangeSet<Exception>> Exceptions,
|
ObservableCollection<Exception> Exceptions,
|
||||||
ReadOnlyExtensionCollection Extensions,
|
ReadOnlyExtensionCollection Extensions,
|
||||||
IObservable<IChangeSet<AbsolutePath, string>> Items) : IContainer
|
ObservableCollection<AbsolutePath> Items) : IContainer
|
||||||
{
|
{
|
||||||
|
|
||||||
private readonly Lazy<ReadOnlyObservableCollection<AbsolutePath>> _itemsCollectionLazy =
|
|
||||||
new(() =>
|
|
||||||
{
|
|
||||||
Items.Bind(out var items).Subscribe();
|
|
||||||
return items;
|
|
||||||
});
|
|
||||||
private readonly CancellationTokenSource _loadingCancellationTokenSource = new();
|
private readonly CancellationTokenSource _loadingCancellationTokenSource = new();
|
||||||
private readonly BehaviorSubject<bool> _isLoading = new(false);
|
private readonly BehaviorSubject<bool> _isLoading = new(false);
|
||||||
|
|
||||||
@@ -42,8 +35,6 @@ public record Container(
|
|||||||
public bool? IsLoaded { get; private set; }
|
public bool? IsLoaded { get; private set; }
|
||||||
public AbsolutePathType Type => AbsolutePathType.Container;
|
public AbsolutePathType Type => AbsolutePathType.Container;
|
||||||
|
|
||||||
public ReadOnlyObservableCollection<AbsolutePath> ItemsCollection => _itemsCollectionLazy.Value;
|
|
||||||
|
|
||||||
public async Task WaitForLoaded(CancellationToken token = default)
|
public async Task WaitForLoaded(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
while (IsLoaded != true) await Task.Delay(1, token);
|
while (IsLoaded != true) await Task.Delay(1, token);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using FileTime.Core.ContentAccess;
|
using FileTime.Core.ContentAccess;
|
||||||
using FileTime.Core.Enums;
|
using FileTime.Core.Enums;
|
||||||
@@ -19,7 +20,7 @@ public record Element(
|
|||||||
string? Attributes,
|
string? Attributes,
|
||||||
IContentProvider Provider,
|
IContentProvider Provider,
|
||||||
PointInTime PointInTime,
|
PointInTime PointInTime,
|
||||||
IObservable<IChangeSet<Exception>> Exceptions,
|
ObservableCollection<Exception> Exceptions,
|
||||||
ReadOnlyExtensionCollection Extensions) : IElement
|
ReadOnlyExtensionCollection Extensions) : IElement
|
||||||
{
|
{
|
||||||
public AbsolutePathType Type => AbsolutePathType.Element;
|
public AbsolutePathType Type => AbsolutePathType.Element;
|
||||||
|
|||||||
@@ -11,12 +11,15 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ObservableComputations" Version="2.3.0" />
|
||||||
<PackageReference Include="System.Interactive.Async" Version="6.0.1" />
|
<PackageReference Include="System.Interactive.Async" Version="6.0.1" />
|
||||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||||
<PackageReference Include="System.Reactive" Version="6.0.0" />
|
<PackageReference Include="System.Reactive" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Library\Defer\Defer.csproj" />
|
||||||
<ProjectReference Include="..\..\Tools\FileTime.Tools\FileTime.Tools.csproj" />
|
<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" />
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
using System.Reactive.Linq;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Subjects;
|
using System.Reactive.Subjects;
|
||||||
|
using DeclarativeProperty;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using DynamicData.Alias;
|
|
||||||
using DynamicData.Binding;
|
using DynamicData.Binding;
|
||||||
|
using FileTime.App.Core.Services;
|
||||||
using FileTime.Core.Helper;
|
using FileTime.Core.Helper;
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
using FileTime.Core.Timeline;
|
using FileTime.Core.Timeline;
|
||||||
|
using ObservableComputations;
|
||||||
|
using static System.DeferTools;
|
||||||
|
|
||||||
namespace FileTime.Core.Services;
|
namespace FileTime.Core.Services;
|
||||||
|
|
||||||
@@ -13,27 +18,46 @@ public class Tab : ITab
|
|||||||
{
|
{
|
||||||
private readonly ITimelessContentProvider _timelessContentProvider;
|
private readonly ITimelessContentProvider _timelessContentProvider;
|
||||||
private readonly ITabEvents _tabEvents;
|
private readonly ITabEvents _tabEvents;
|
||||||
private readonly BehaviorSubject<IContainer?> _currentLocation = new(null);
|
private readonly IRefreshSmoothnessCalculator _refreshSmoothnessCalculator;
|
||||||
|
private readonly DeclarativeProperty<IContainer?> _currentLocation = new(null);
|
||||||
private readonly BehaviorSubject<IContainer?> _currentLocationForced = new(null);
|
private readonly BehaviorSubject<IContainer?> _currentLocationForced = new(null);
|
||||||
private readonly BehaviorSubject<AbsolutePath?> _currentSelectedItem = new(null);
|
private readonly DeclarativeProperty<AbsolutePath?> _currentRequestItem = new(null);
|
||||||
private readonly SourceList<ItemFilter> _itemFilters = new();
|
private readonly SourceList<ItemFilter> _itemFilters = new();
|
||||||
private AbsolutePath? _currentSelectedItemCached;
|
private AbsolutePath? _currentSelectedItemCached;
|
||||||
private PointInTime _currentPointInTime;
|
private PointInTime _currentPointInTime;
|
||||||
|
private OcConsumer? _currentItemsConsumer;
|
||||||
|
private CancellationTokenSource? _setCurrentLocationCancellationTokenSource;
|
||||||
|
private CancellationTokenSource? _setCurrentItemCancellationTokenSource;
|
||||||
|
|
||||||
public IObservable<IContainer?> CurrentLocation { get; }
|
public IDeclarativeProperty<IContainer?> CurrentLocation { get; }
|
||||||
public IObservable<IObservable<IChangeSet<IItem, string>>?> CurrentItems { get; }
|
public IDeclarativeProperty<ObservableCollection<IItem>?> CurrentItems { get; }
|
||||||
public IObservable<AbsolutePath?> CurrentSelectedItem { get; }
|
public IDeclarativeProperty<AbsolutePath?> CurrentSelectedItem { get; }
|
||||||
public FullName? LastDeepestSelectedPath { get; private set; }
|
public FullName? LastDeepestSelectedPath { get; private set; }
|
||||||
|
|
||||||
public Tab(ITimelessContentProvider timelessContentProvider, ITabEvents tabEvents)
|
public Tab(
|
||||||
|
ITimelessContentProvider timelessContentProvider,
|
||||||
|
ITabEvents tabEvents,
|
||||||
|
IRefreshSmoothnessCalculator refreshSmoothnessCalculator)
|
||||||
{
|
{
|
||||||
_timelessContentProvider = timelessContentProvider;
|
_timelessContentProvider = timelessContentProvider;
|
||||||
_tabEvents = tabEvents;
|
_tabEvents = tabEvents;
|
||||||
|
_refreshSmoothnessCalculator = refreshSmoothnessCalculator;
|
||||||
_currentPointInTime = null!;
|
_currentPointInTime = null!;
|
||||||
|
|
||||||
_timelessContentProvider.CurrentPointInTime.Subscribe(p => _currentPointInTime = p);
|
_timelessContentProvider.CurrentPointInTime.Subscribe(p => _currentPointInTime = p);
|
||||||
|
|
||||||
CurrentLocation = _currentLocation
|
CurrentLocation = _currentLocation;
|
||||||
|
CurrentLocation.Subscribe((c, _) =>
|
||||||
|
{
|
||||||
|
if (_currentSelectedItemCached is not null)
|
||||||
|
{
|
||||||
|
LastDeepestSelectedPath = FullName.CreateSafe(PathHelper.GetLongerPath(LastDeepestSelectedPath?.Path, _currentSelectedItemCached.Path.Path));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
/*CurrentLocation = _currentLocation
|
||||||
.DistinctUntilChanged()
|
.DistinctUntilChanged()
|
||||||
.Merge(_currentLocationForced)
|
.Merge(_currentLocationForced)
|
||||||
.Do(_ =>
|
.Do(_ =>
|
||||||
@@ -44,14 +68,43 @@ public class Tab : ITab
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.Publish(null)
|
.Publish(null)
|
||||||
.RefCount();
|
.RefCount();*/
|
||||||
|
|
||||||
CurrentItems =
|
CurrentItems = CurrentLocation.Map((container, _) =>
|
||||||
|
{
|
||||||
|
var items = container is null
|
||||||
|
? (ObservableCollection<IItem>?) null
|
||||||
|
: container.Items.Selecting<AbsolutePath, IItem>(i => MapItem(i));
|
||||||
|
return Task.FromResult(items);
|
||||||
|
}
|
||||||
|
) /*.Watch<ObservableCollection<IItem>, IItem>()*/;
|
||||||
|
/*using var _ = Defer(() =>
|
||||||
|
CurrentItems.Subscribe(c => UpdateConsumer(c, ref _currentItemsConsumer))
|
||||||
|
);*/
|
||||||
|
|
||||||
|
/*CurrentItems.RegisterTrigger(
|
||||||
|
(sender, items) =>
|
||||||
|
{
|
||||||
|
if (items is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
items.CollectionChanged += Handler;
|
||||||
|
|
||||||
|
return Disposable.Create(() => items.CollectionChanged -= Handler);
|
||||||
|
|
||||||
|
void Handler(object? o, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var t = Task.Run(async () => await sender.ReFireAsync());
|
||||||
|
t.Wait();
|
||||||
|
}
|
||||||
|
});*/
|
||||||
|
|
||||||
|
/*CurrentItems =
|
||||||
Observable.Merge(
|
Observable.Merge(
|
||||||
Observable.CombineLatest(
|
Observable.CombineLatest(
|
||||||
CurrentLocation
|
CurrentLocation
|
||||||
.Where(c => c is not null)
|
.Where(c => c is not null)
|
||||||
.Select(c => c!.Items)
|
.Select(c => c!.ItemsCollection)
|
||||||
.Select(items => items.TransformAsync(MapItem)),
|
.Select(items => items.TransformAsync(MapItem)),
|
||||||
_itemFilters.Connect().StartWithEmpty().ToCollection(),
|
_itemFilters.Connect().StartWithEmpty().ToCollection(),
|
||||||
(items, filters) =>
|
(items, filters) =>
|
||||||
@@ -66,9 +119,26 @@ public class Tab : ITab
|
|||||||
.Select(_ => (IObservable<IChangeSet<IItem, string>>?) null)
|
.Select(_ => (IObservable<IChangeSet<IItem, string>>?) null)
|
||||||
)
|
)
|
||||||
.Publish(null)
|
.Publish(null)
|
||||||
.RefCount();
|
.RefCount();*/
|
||||||
|
CurrentSelectedItem = DeclarativePropertyHelpers.CombineLatest(
|
||||||
|
CurrentItems.Watch<ObservableCollection<IItem>, IItem>(),
|
||||||
|
_currentRequestItem.DistinctUntilChanged(),
|
||||||
|
(items, selected) =>
|
||||||
|
{
|
||||||
|
if (selected != null && (items?.Any(i => i.FullName == selected.Path) ?? true)) return Task.FromResult<AbsolutePath?>(selected);
|
||||||
|
if (items == null || items.Count == 0) return Task.FromResult<AbsolutePath?>(null);
|
||||||
|
|
||||||
CurrentSelectedItem =
|
return Task.FromResult(GetSelectedItemByItems(items));
|
||||||
|
}).DistinctUntilChanged();
|
||||||
|
|
||||||
|
CurrentSelectedItem.Subscribe((v) =>
|
||||||
|
{
|
||||||
|
_refreshSmoothnessCalculator.RegisterChange();
|
||||||
|
_refreshSmoothnessCalculator.RecalculateSmoothness();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*CurrentSelectedItem =
|
||||||
Observable.CombineLatest(
|
Observable.CombineLatest(
|
||||||
CurrentItems
|
CurrentItems
|
||||||
.Select(c =>
|
.Select(c =>
|
||||||
@@ -88,27 +158,40 @@ public class Tab : ITab
|
|||||||
)
|
)
|
||||||
.DistinctUntilChanged()
|
.DistinctUntilChanged()
|
||||||
.Publish(null)
|
.Publish(null)
|
||||||
.RefCount();
|
.RefCount();*/
|
||||||
|
|
||||||
CurrentSelectedItem.Subscribe(s =>
|
CurrentSelectedItem.Subscribe(async (s, _) =>
|
||||||
{
|
{
|
||||||
_currentSelectedItemCached = s;
|
_currentSelectedItemCached = s;
|
||||||
_currentSelectedItem.OnNext(s);
|
await _currentRequestItem.SetValue(s);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void UpdateConsumer<T>(ObservableCollection<T>? collection, ref OcConsumer? consumer)
|
||||||
|
{
|
||||||
|
if (collection is not IComputing computing) return;
|
||||||
|
|
||||||
|
consumer?.Dispose();
|
||||||
|
consumer = new OcConsumer();
|
||||||
|
computing.For(consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IItem MapItem(AbsolutePath item)
|
||||||
|
{
|
||||||
|
var t = Task.Run(async () => await item.ResolveAsync(true));
|
||||||
|
t.Wait();
|
||||||
|
return t.Result;
|
||||||
|
}
|
||||||
|
|
||||||
private static SortExpressionComparer<IItem> SortItems()
|
private static SortExpressionComparer<IItem> SortItems()
|
||||||
//TODO: Order
|
//TODO: Order
|
||||||
=> SortExpressionComparer<IItem>
|
=> SortExpressionComparer<IItem>
|
||||||
.Ascending(i => i.Type)
|
.Ascending(i => i.Type)
|
||||||
.ThenByAscending(i => i.DisplayName.ToLower());
|
.ThenByAscending(i => i.DisplayName.ToLower());
|
||||||
|
|
||||||
private async Task<IItem> MapItem(AbsolutePath item) => await item.ResolveAsync(true);
|
|
||||||
|
|
||||||
public void Init(IContainer currentLocation)
|
public async Task InitAsync(IContainer currentLocation)
|
||||||
{
|
=> await _currentLocation.SetValue(currentLocation);
|
||||||
_currentLocation.OnNext(currentLocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private AbsolutePath? GetSelectedItemByItems(IReadOnlyCollection<IItem> items)
|
private AbsolutePath? GetSelectedItemByItems(IReadOnlyCollection<IItem> items)
|
||||||
{
|
{
|
||||||
@@ -140,9 +223,11 @@ public class Tab : ITab
|
|||||||
return newSelectedItem;
|
return newSelectedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetCurrentLocation(IContainer newLocation)
|
public async Task SetCurrentLocation(IContainer newLocation)
|
||||||
{
|
{
|
||||||
_currentLocation.OnNext(newLocation);
|
_setCurrentLocationCancellationTokenSource?.Cancel();
|
||||||
|
_setCurrentLocationCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
await _currentLocation.SetValue(newLocation, _setCurrentLocationCancellationTokenSource.Token);
|
||||||
|
|
||||||
if (newLocation.FullName != null)
|
if (newLocation.FullName != null)
|
||||||
{
|
{
|
||||||
@@ -150,9 +235,11 @@ public class Tab : ITab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ForceSetCurrentLocation(IContainer newLocation)
|
public async Task ForceSetCurrentLocation(IContainer newLocation)
|
||||||
{
|
{
|
||||||
_currentLocationForced.OnNext(newLocation);
|
_setCurrentLocationCancellationTokenSource?.Cancel();
|
||||||
|
_setCurrentLocationCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
await _currentLocation.SetValue(newLocation, _setCurrentLocationCancellationTokenSource.Token);
|
||||||
|
|
||||||
if (newLocation.FullName != null)
|
if (newLocation.FullName != null)
|
||||||
{
|
{
|
||||||
@@ -160,7 +247,12 @@ public class Tab : ITab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetSelectedItem(AbsolutePath newSelectedItem) => _currentSelectedItem.OnNext(newSelectedItem);
|
public async Task SetSelectedItem(AbsolutePath newSelectedItem)
|
||||||
|
{
|
||||||
|
_setCurrentItemCancellationTokenSource?.Cancel();
|
||||||
|
_setCurrentItemCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
await _currentRequestItem.SetValue(newSelectedItem, _setCurrentItemCancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
public void AddItemFilter(ItemFilter filter) => _itemFilters.Add(filter);
|
public void AddItemFilter(ItemFilter filter) => _itemFilters.Add(filter);
|
||||||
public void RemoveItemFilter(ItemFilter filter) => _itemFilters.Remove(filter);
|
public void RemoveItemFilter(ItemFilter filter) => _itemFilters.Remove(filter);
|
||||||
@@ -178,13 +270,12 @@ public class Tab : ITab
|
|||||||
await _currentSelectedItemCached.TimelessProvider.GetItemByFullNameAsync(_currentSelectedItemCached.Path, _currentPointInTime);
|
await _currentSelectedItemCached.TimelessProvider.GetItemByFullNameAsync(_currentSelectedItemCached.Path, _currentPointInTime);
|
||||||
|
|
||||||
if (resolvedSelectedItem is not IContainer resolvedContainer) return;
|
if (resolvedSelectedItem is not IContainer resolvedContainer) return;
|
||||||
SetCurrentLocation(resolvedContainer);
|
await SetCurrentLocation(resolvedContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_currentLocation.Dispose();
|
_currentLocation.Dispose();
|
||||||
_currentSelectedItem.Dispose();
|
|
||||||
_itemFilters.Dispose();
|
_itemFilters.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,6 +81,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.App.FuzzyPanel.Abs
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.GuiApp.DesignPreview", "GuiApp\Avalonia\FileTime.GuiApp.DesignPreview\FileTime.GuiApp.DesignPreview.csproj", "{D0EC224E-F043-4657-BD6A-1ADE52DFF8B5}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.GuiApp.DesignPreview", "GuiApp\Avalonia\FileTime.GuiApp.DesignPreview\FileTime.GuiApp.DesignPreview.csproj", "{D0EC224E-F043-4657-BD6A-1ADE52DFF8B5}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeclarativeProperty", "Library\DeclarativeProperty\DeclarativeProperty.csproj", "{D5C85B69-6345-4FAA-A00C-3A73BA677664}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Defer", "Library\Defer\Defer.csproj", "{609FFADA-C221-4E41-B377-C6AF56C0A900}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -211,6 +215,14 @@ Global
|
|||||||
{D0EC224E-F043-4657-BD6A-1ADE52DFF8B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D0EC224E-F043-4657-BD6A-1ADE52DFF8B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D0EC224E-F043-4657-BD6A-1ADE52DFF8B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D0EC224E-F043-4657-BD6A-1ADE52DFF8B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D0EC224E-F043-4657-BD6A-1ADE52DFF8B5}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D0EC224E-F043-4657-BD6A-1ADE52DFF8B5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D5C85B69-6345-4FAA-A00C-3A73BA677664}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D5C85B69-6345-4FAA-A00C-3A73BA677664}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D5C85B69-6345-4FAA-A00C-3A73BA677664}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D5C85B69-6345-4FAA-A00C-3A73BA677664}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{609FFADA-C221-4E41-B377-C6AF56C0A900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{609FFADA-C221-4E41-B377-C6AF56C0A900}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{609FFADA-C221-4E41-B377-C6AF56C0A900}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{609FFADA-C221-4E41-B377-C6AF56C0A900}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -248,6 +260,8 @@ Global
|
|||||||
{7FDCE43D-D084-4539-B797-8A72D4DD610D} = {A5291117-3001-498B-AC8B-E14F71F72570}
|
{7FDCE43D-D084-4539-B797-8A72D4DD610D} = {A5291117-3001-498B-AC8B-E14F71F72570}
|
||||||
{7690E4EA-DA2A-45B4-AD83-80EE07A05169} = {A5291117-3001-498B-AC8B-E14F71F72570}
|
{7690E4EA-DA2A-45B4-AD83-80EE07A05169} = {A5291117-3001-498B-AC8B-E14F71F72570}
|
||||||
{D0EC224E-F043-4657-BD6A-1ADE52DFF8B5} = {01F231DE-4A65-435F-B4BB-77EE5221890C}
|
{D0EC224E-F043-4657-BD6A-1ADE52DFF8B5} = {01F231DE-4A65-435F-B4BB-77EE5221890C}
|
||||||
|
{D5C85B69-6345-4FAA-A00C-3A73BA677664} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
|
||||||
|
{609FFADA-C221-4E41-B377-C6AF56C0A900} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}
|
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using DeclarativeProperty;
|
||||||
using FileTime.App.Core.ViewModels;
|
using FileTime.App.Core.ViewModels;
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
using FileTime.GuiApp.Configuration;
|
using FileTime.GuiApp.Configuration;
|
||||||
@@ -12,7 +13,7 @@ public interface IGuiAppState : IAppState
|
|||||||
bool IsAllShortcutVisible { get; set; }
|
bool IsAllShortcutVisible { get; set; }
|
||||||
bool NoCommandFound { get; set; }
|
bool NoCommandFound { get; set; }
|
||||||
List<CommandBindingConfiguration> PossibleCommands { get; set; }
|
List<CommandBindingConfiguration> PossibleCommands { get; set; }
|
||||||
BindedCollection<RootDriveInfo, string> RootDriveInfos { get; set; }
|
ObservableCollection<RootDriveInfo> RootDriveInfos { get; set; }
|
||||||
IReadOnlyList<PlaceInfo> Places { get; set; }
|
IReadOnlyList<PlaceInfo> Places { get; set; }
|
||||||
ObservableCollection<string> PopupTexts { get; }
|
ObservableCollection<string> PopupTexts { get; }
|
||||||
IObservable<GuiPanel> ActivePanel { get; }
|
IObservable<GuiPanel> ActivePanel { get; }
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public partial class GuiAppState : AppStateBase, IGuiAppState, IDisposable
|
|||||||
|
|
||||||
[Property] private List<CommandBindingConfiguration> _possibleCommands = new();
|
[Property] private List<CommandBindingConfiguration> _possibleCommands = new();
|
||||||
|
|
||||||
[Property] private BindedCollection<RootDriveInfo, string> _rootDriveInfos = new();
|
[Property] private ObservableCollection<RootDriveInfo> _rootDriveInfos = new();
|
||||||
|
|
||||||
[Property] private IReadOnlyList<PlaceInfo> _places = new List<PlaceInfo>();
|
[Property] private IReadOnlyList<PlaceInfo> _places = new List<PlaceInfo>();
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ using FileTime.GuiApp.ViewModels;
|
|||||||
|
|
||||||
namespace FileTime.GuiApp.DesignPreview.Services;
|
namespace FileTime.GuiApp.DesignPreview.Services;
|
||||||
|
|
||||||
public class GuiAppStatePreview : IGuiAppState
|
/*public class GuiAppStatePreview : IGuiAppState
|
||||||
{
|
{
|
||||||
public GuiAppStatePreview()
|
public GuiAppStatePreview()
|
||||||
{
|
{
|
||||||
@@ -19,7 +19,7 @@ public class GuiAppStatePreview : IGuiAppState
|
|||||||
CurrentSelectedTab = tab;
|
CurrentSelectedTab = tab;
|
||||||
|
|
||||||
var tabs = new ObservableCollection<ITabViewModel>(new [] {tab});
|
var tabs = new ObservableCollection<ITabViewModel>(new [] {tab});
|
||||||
Tabs = new ReadOnlyObservableCollection<ITabViewModel>(tabs);
|
Tabs = new ObservableCollection<ITabViewModel>(tabs);
|
||||||
|
|
||||||
SearchText = new BehaviorSubject<string?>(null);
|
SearchText = new BehaviorSubject<string?>(null);
|
||||||
ViewMode = new BehaviorSubject<ViewMode>(FileTime.App.Core.Models.Enums.ViewMode.Default);
|
ViewMode = new BehaviorSubject<ViewMode>(FileTime.App.Core.Models.Enums.ViewMode.Default);
|
||||||
@@ -30,7 +30,7 @@ public class GuiAppStatePreview : IGuiAppState
|
|||||||
PopupTexts = new ObservableCollection<string>();
|
PopupTexts = new ObservableCollection<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlyObservableCollection<ITabViewModel> Tabs { get; }
|
public ObservableCollection<ITabViewModel> Tabs { get; }
|
||||||
public IObservable<ITabViewModel> SelectedTab { get; }
|
public IObservable<ITabViewModel> SelectedTab { get; }
|
||||||
public IObservable<string?> SearchText { get; }
|
public IObservable<string?> SearchText { get; }
|
||||||
public IObservable<ViewMode> ViewMode { get; }
|
public IObservable<ViewMode> ViewMode { get; }
|
||||||
@@ -56,4 +56,4 @@ public class GuiAppStatePreview : IGuiAppState
|
|||||||
public ObservableCollection<string> PopupTexts { get; }
|
public ObservableCollection<string> PopupTexts { get; }
|
||||||
public IObservable<GuiPanel> ActivePanel { get; }
|
public IObservable<GuiPanel> ActivePanel { get; }
|
||||||
public void SetActivePanel(GuiPanel newPanel) => throw new NotImplementedException();
|
public void SetActivePanel(GuiPanel newPanel) => throw new NotImplementedException();
|
||||||
}
|
}*/
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using DynamicData;
|
using System.Collections.ObjectModel;
|
||||||
|
using DynamicData;
|
||||||
using FileTime.Core.Enums;
|
using FileTime.Core.Enums;
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
using FileTime.Core.Timeline;
|
using FileTime.Core.Timeline;
|
||||||
@@ -11,7 +12,7 @@ public class ItemPreview
|
|||||||
|
|
||||||
static ItemPreview()
|
static ItemPreview()
|
||||||
{
|
{
|
||||||
var exceptions = new SourceList<Exception>();
|
var exceptions = new ObservableCollection<Exception>();
|
||||||
CurrentContainer = new Container(
|
CurrentContainer = new Container(
|
||||||
"HomePreview",
|
"HomePreview",
|
||||||
"HomePreview",
|
"HomePreview",
|
||||||
@@ -32,9 +33,9 @@ public class ItemPreview
|
|||||||
null!,
|
null!,
|
||||||
false,
|
false,
|
||||||
PointInTime.Present,
|
PointInTime.Present,
|
||||||
exceptions.Connect(),
|
exceptions,
|
||||||
new ExtensionCollection().AsReadOnly(),
|
new ExtensionCollection().AsReadOnly(),
|
||||||
new SourceCache<AbsolutePath, string>(a => a.Path.Path).Connect()
|
new ObservableCollection<AbsolutePath>()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ public class ItemPreview
|
|||||||
"attr",
|
"attr",
|
||||||
null!,
|
null!,
|
||||||
PointInTime.Present,
|
PointInTime.Present,
|
||||||
new SourceList<Exception>().Connect(),
|
new ObservableCollection<Exception>(),
|
||||||
new ExtensionCollection().AsReadOnly()
|
new ExtensionCollection().AsReadOnly()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ using FileTime.Core.Services;
|
|||||||
|
|
||||||
namespace FileTime.GuiApp.DesignPreview.Services;
|
namespace FileTime.GuiApp.DesignPreview.Services;
|
||||||
|
|
||||||
public class TabPreview : ITab
|
/*public class TabPreview : ITab
|
||||||
{
|
{
|
||||||
public TabPreview()
|
public TabPreview()
|
||||||
{
|
{
|
||||||
@@ -46,4 +46,4 @@ public class TabPreview : ITab
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
@@ -9,7 +9,7 @@ using FileTime.Core.Services;
|
|||||||
|
|
||||||
namespace FileTime.GuiApp.DesignPreview.Services;
|
namespace FileTime.GuiApp.DesignPreview.Services;
|
||||||
|
|
||||||
public class TabViewModelPreview : ITabViewModel
|
/*public class TabViewModelPreview : ITabViewModel
|
||||||
{
|
{
|
||||||
private static readonly ItemNameConverterService _itemNameConverterService = new();
|
private static readonly ItemNameConverterService _itemNameConverterService = new();
|
||||||
|
|
||||||
@@ -115,4 +115,4 @@ public class TabViewModelPreview : ITabViewModel
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<Styles
|
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
|
||||||
<Styles.Resources>
|
<Styles.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
@@ -21,6 +19,9 @@
|
|||||||
<Style Selector="TextBlock.ExtraSmallText">
|
<Style Selector="TextBlock.ExtraSmallText">
|
||||||
<Setter Property="FontSize" Value="11" />
|
<Setter Property="FontSize" Value="11" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="TextBlock.ErrorText">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ErrorBrush}" />
|
||||||
|
</Style>
|
||||||
<Style Selector="TextBox">
|
<Style Selector="TextBox">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource ForegroundBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource ForegroundBrush}" />
|
||||||
<Setter Property="Background" Value="{DynamicResource ContainerBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource ContainerBackgroundBrush}" />
|
||||||
@@ -77,10 +78,10 @@
|
|||||||
<ControlTemplate>
|
<ControlTemplate>
|
||||||
<Border>
|
<Border>
|
||||||
<RadioButton
|
<RadioButton
|
||||||
VerticalAlignment="Center"
|
|
||||||
Content="{TemplateBinding ContentPresenter.Content}"
|
Content="{TemplateBinding ContentPresenter.Content}"
|
||||||
Foreground="{DynamicResource ForegroundBrush}"
|
Foreground="{DynamicResource ForegroundBrush}"
|
||||||
IsChecked="{Binding Path=IsSelected, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
|
IsChecked="{Binding Path=IsSelected, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
</Border>
|
</Border>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
@@ -90,9 +91,9 @@
|
|||||||
<Style Selector="Image.LoadingAnimation">
|
<Style Selector="Image.LoadingAnimation">
|
||||||
<Style.Animations>
|
<Style.Animations>
|
||||||
<Animation
|
<Animation
|
||||||
|
Duration="0:0:2"
|
||||||
Easing="QuadraticEaseInOut"
|
Easing="QuadraticEaseInOut"
|
||||||
IterationCount="INFINITE"
|
IterationCount="INFINITE">
|
||||||
Duration="0:0:2">
|
|
||||||
<KeyFrame Cue="0%">
|
<KeyFrame Cue="0%">
|
||||||
<Setter Property="RotateTransform.Angle" Value="45" />
|
<Setter Property="RotateTransform.Angle" Value="45" />
|
||||||
</KeyFrame>
|
</KeyFrame>
|
||||||
|
|||||||
@@ -1,47 +1,37 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using DynamicData;
|
|
||||||
using DynamicData.Binding;
|
|
||||||
using FileTime.App.Core.Services;
|
using FileTime.App.Core.Services;
|
||||||
using FileTime.Core.Extensions;
|
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
using FileTime.GuiApp.ViewModels;
|
using FileTime.GuiApp.ViewModels;
|
||||||
using FileTime.Providers.Local;
|
using FileTime.Providers.Local;
|
||||||
|
using ObservableComputations;
|
||||||
|
|
||||||
namespace FileTime.GuiApp.Services;
|
namespace FileTime.GuiApp.Services;
|
||||||
|
|
||||||
public class RootDriveInfoService : IStartupHandler
|
public class RootDriveInfoService : IStartupHandler, IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly ILocalContentProvider _localContentProvider;
|
||||||
private readonly List<DriveInfo> _rootDrives = new();
|
private readonly List<DriveInfo> _rootDrives = new();
|
||||||
|
private readonly OcConsumer _rootDriveInfosConsumer = new();
|
||||||
|
|
||||||
public RootDriveInfoService(
|
public RootDriveInfoService(
|
||||||
IGuiAppState guiAppState,
|
IGuiAppState guiAppState,
|
||||||
ILocalContentProvider localContentProvider)
|
ILocalContentProvider localContentProvider)
|
||||||
{
|
{
|
||||||
|
_localContentProvider = localContentProvider;
|
||||||
InitRootDrives();
|
InitRootDrives();
|
||||||
|
|
||||||
var rootDriveInfos = localContentProvider.Items.Transform(
|
var rootDriveInfos = localContentProvider.Items.Selecting<AbsolutePath, (AbsolutePath Path, DriveInfo? Drive)>(
|
||||||
i =>
|
i => MatchRootDrive(i)
|
||||||
{
|
|
||||||
var rootDrive = _rootDrives.FirstOrDefault(d =>
|
|
||||||
{
|
|
||||||
var containerPath = localContentProvider.GetNativePath(i.Path).Path;
|
|
||||||
var drivePath = d.Name.TrimEnd(Path.DirectorySeparatorChar);
|
|
||||||
return containerPath == drivePath
|
|
||||||
|| (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && containerPath == "/" &&
|
|
||||||
d.Name == "/");
|
|
||||||
});
|
|
||||||
|
|
||||||
return (Path: i, Drive: rootDrive);
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.Filter(t => t.Drive is not null)
|
.Filtering(t => IsNotNull(t.Drive))
|
||||||
.TransformAsync(async t => (Item: await t.Path.ResolveAsyncSafe(), Drive: t.Drive!))
|
.Selecting(t => Resolve(t))
|
||||||
.Filter(t => t.Item is IContainer)
|
.Filtering(t => t.Item is IContainer)
|
||||||
.Transform(t => (Container: (IContainer) t.Item!, t.Drive))
|
.Selecting(t => new RootDriveInfo(t.Drive, (IContainer)t.Item!))
|
||||||
.Transform(t => new RootDriveInfo(t.Drive, t.Container))
|
.Ordering(d => d.Name);
|
||||||
.Sort(SortExpressionComparer<RootDriveInfo>.Ascending(d => d.Name));
|
|
||||||
|
|
||||||
guiAppState.RootDriveInfos = rootDriveInfos.ToBindedCollection();
|
rootDriveInfos.For(_rootDriveInfosConsumer);
|
||||||
|
|
||||||
|
guiAppState.RootDriveInfos = rootDriveInfos;
|
||||||
|
|
||||||
void InitRootDrives()
|
void InitRootDrives()
|
||||||
{
|
{
|
||||||
@@ -60,5 +50,29 @@ public class RootDriveInfoService : IStartupHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task InitAsync() => Task.CompletedTask;
|
private static bool IsNotNull(object? obj) => obj is not null;
|
||||||
|
|
||||||
|
private static (IItem? Item, DriveInfo Drive) Resolve((AbsolutePath Path, DriveInfo? Drive) tuple)
|
||||||
|
{
|
||||||
|
var t = Task.Run(async () => await tuple.Path.ResolveAsyncSafe());
|
||||||
|
t.Wait();
|
||||||
|
return (Item: t.Result, Drive: tuple.Drive!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (AbsolutePath Path, DriveInfo? Drive) MatchRootDrive(AbsolutePath sourceItem)
|
||||||
|
{
|
||||||
|
var rootDrive = _rootDrives.FirstOrDefault(d =>
|
||||||
|
{
|
||||||
|
var containerPath = _localContentProvider.GetNativePath(sourceItem.Path).Path;
|
||||||
|
var drivePath = d.Name.TrimEnd(Path.DirectorySeparatorChar);
|
||||||
|
return containerPath == drivePath
|
||||||
|
|| (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && containerPath == "/" &&
|
||||||
|
d.Name == "/");
|
||||||
|
});
|
||||||
|
|
||||||
|
return (Path: sourceItem, Drive: rootDrive);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InitAsync() => Task.CompletedTask;
|
||||||
|
public void Dispose() => _rootDriveInfosConsumer.Dispose();
|
||||||
}
|
}
|
||||||
@@ -13,4 +13,5 @@ public interface IMainWindowViewModel : IMainWindowViewModelBase
|
|||||||
IDialogService DialogService { get; }
|
IDialogService DialogService { get; }
|
||||||
IFrequencyNavigationService FrequencyNavigationService { get; }
|
IFrequencyNavigationService FrequencyNavigationService { get; }
|
||||||
ICommandPaletteService CommandPaletteService { get; }
|
ICommandPaletteService CommandPaletteService { get; }
|
||||||
|
public IRefreshSmoothnessCalculator RefreshSmoothnessCalculator { get; }
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ using FileTime.GuiApp.Services;
|
|||||||
|
|
||||||
namespace FileTime.GuiApp.ViewModels;
|
namespace FileTime.GuiApp.ViewModels;
|
||||||
|
|
||||||
public class MainWindowDesignViewModel : IMainWindowViewModel
|
public class MainWindowDesignViewModel //: IMainWindowViewModel
|
||||||
{
|
{
|
||||||
public bool Loading => false;
|
public bool Loading => false;
|
||||||
public IObservable<string?> MainFont { get; } = new BehaviorSubject<string?>(null);
|
public IObservable<string?> MainFont { get; } = new BehaviorSubject<string?>(null);
|
||||||
@@ -20,6 +20,6 @@ public class MainWindowDesignViewModel : IMainWindowViewModel
|
|||||||
|
|
||||||
public MainWindowDesignViewModel()
|
public MainWindowDesignViewModel()
|
||||||
{
|
{
|
||||||
AppState = new GuiAppStatePreview();
|
//AppState = new GuiAppStatePreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ namespace FileTime.GuiApp.ViewModels;
|
|||||||
[Inject(typeof(IFontService), "_fontService")]
|
[Inject(typeof(IFontService), "_fontService")]
|
||||||
[Inject(typeof(IFrequencyNavigationService), PropertyAccessModifier = AccessModifier.Public)]
|
[Inject(typeof(IFrequencyNavigationService), PropertyAccessModifier = AccessModifier.Public)]
|
||||||
[Inject(typeof(ICommandPaletteService), PropertyAccessModifier = AccessModifier.Public)]
|
[Inject(typeof(ICommandPaletteService), PropertyAccessModifier = AccessModifier.Public)]
|
||||||
|
[Inject(typeof(IRefreshSmoothnessCalculator), PropertyAccessModifier = AccessModifier.Public)]
|
||||||
public partial class MainWindowViewModel : IMainWindowViewModel
|
public partial class MainWindowViewModel : IMainWindowViewModel
|
||||||
{
|
{
|
||||||
public bool Loading => false;
|
public bool Loading => false;
|
||||||
|
|||||||
@@ -49,9 +49,14 @@
|
|||||||
|
|
||||||
<Grid ColumnDefinitions="250,*" RowDefinitions="Auto,*">
|
<Grid ColumnDefinitions="250,*" RowDefinitions="Auto,*">
|
||||||
|
|
||||||
<Grid PointerPressed="HeaderPointerPressed">
|
<!--<Grid PointerPressed="HeaderPointerPressed">
|
||||||
<Rectangle Fill="#01000000" />
|
<Rectangle Fill="#01000000" />
|
||||||
<TextBlock Margin="15,10" Text="{Binding Title}" />
|
<TextBlock Margin="15,10" Text="{Binding Title}" />
|
||||||
|
</Grid>-->
|
||||||
|
|
||||||
|
<Grid PointerPressed="HeaderPointerPressed">
|
||||||
|
<Rectangle Fill="#01000000" />
|
||||||
|
<TextBlock Margin="15,10" Text="{Binding RefreshSmoothnessCalculator.RefreshDelay}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Column="1" PointerPressed="HeaderPointerPressed">
|
<Grid Grid.Column="1" PointerPressed="HeaderPointerPressed">
|
||||||
@@ -59,7 +64,7 @@
|
|||||||
|
|
||||||
<StackPanel Margin="20,10" Orientation="Horizontal">
|
<StackPanel Margin="20,10" Orientation="Horizontal">
|
||||||
<local:PathPresenter DataContext="{Binding AppState.SelectedTab^.CurrentLocation^.FullName.Path, Converter={StaticResource PathPreformatter}}" />
|
<local:PathPresenter DataContext="{Binding AppState.SelectedTab^.CurrentLocation^.FullName.Path, Converter={StaticResource PathPreformatter}}" />
|
||||||
<TextBlock Foreground="{StaticResource AccentBrush}" Text="{Binding AppState.SelectedTab^.CurrentSelectedItem^.DisplayNameText}" />
|
<TextBlock Foreground="{StaticResource AccentBrush}" Text="{Binding AppState.SelectedTab^.CurrentSelectedItem.Value.DisplayNameText}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -74,7 +79,7 @@
|
|||||||
|
|
||||||
<TextBlock Margin="0,0,0,10" Text="Drives" />
|
<TextBlock Margin="0,0,0,10" Text="Drives" />
|
||||||
|
|
||||||
<ItemsRepeater Grid.Row="1" ItemsSource="{Binding AppState.RootDriveInfos.Collection}">
|
<ItemsRepeater Grid.Row="1" ItemsSource="{Binding AppState.RootDriveInfos}">
|
||||||
<ItemsRepeater.ItemTemplate>
|
<ItemsRepeater.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:RootDriveInfo">
|
<DataTemplate x:DataType="vm:RootDriveInfo">
|
||||||
<Grid
|
<Grid
|
||||||
@@ -356,7 +361,7 @@
|
|||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="5,0,0,0"
|
Margin="5,0,0,0"
|
||||||
Text="{Binding CurrentLocation^.Name, FallbackValue=Loading...}"
|
Text="{Binding CurrentLocation.Value.Name, FallbackValue=Loading...}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
@@ -376,10 +381,9 @@
|
|||||||
AutoScrollToSelectedItem="True"
|
AutoScrollToSelectedItem="True"
|
||||||
Classes="ContentListView"
|
Classes="ContentListView"
|
||||||
IsTabStop="True"
|
IsTabStop="True"
|
||||||
ItemsSource="{Binding AppState.SelectedTab^.ParentsChildrenCollection.Collection}"
|
ItemsSource="{Binding AppState.SelectedTab^.ParentsChildren.Value}"
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
ScrollViewer.VerticalScrollBarVisibility="Visible">
|
||||||
x:CompileBindings="False">
|
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="corevm:IItemViewModel">
|
<DataTemplate x:DataType="corevm:IItemViewModel">
|
||||||
<local:ItemView
|
<local:ItemView
|
||||||
@@ -399,33 +403,22 @@
|
|||||||
Width="1" />
|
Width="1" />
|
||||||
|
|
||||||
<Grid Grid.Column="2" RowDefinitions="Auto,*">
|
<Grid Grid.Column="2" RowDefinitions="Auto,*">
|
||||||
<Grid IsVisible="{Binding AppState.SelectedTab^.CurrentLocation^.IsLoading^, FallbackValue=False}">
|
<Grid IsVisible="{Binding AppState.SelectedTab^.CurrentLocation.Value.IsLoading^, FallbackValue=False}">
|
||||||
<Image
|
<Image
|
||||||
Classes="LoadingAnimation"
|
Classes="LoadingAnimation"
|
||||||
Height="40"
|
Height="40"
|
||||||
Source="{SvgImage /Assets/loading.svg}"
|
Source="{SvgImage /Assets/loading.svg}"
|
||||||
Width="40">
|
Width="40" />
|
||||||
<!-- https://github.com/AvaloniaUI/Avalonia/issues/8791 -->
|
|
||||||
<!-- Has to explicitly set RotateTransform or animation does not work -->
|
|
||||||
<Image.RenderTransform>
|
|
||||||
<TransformGroup>
|
|
||||||
<RotateTransform />
|
|
||||||
</TransformGroup>
|
|
||||||
</Image.RenderTransform>
|
|
||||||
</Image>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<!-- Note: On Windows CurrentItemsCollection is not sorted (for some reason) -->
|
|
||||||
<!-- Although CurrentItemsCollectionObservable is slower, but it is sorted -->
|
|
||||||
<!-- This should be fixed (works on linux tho...) -->
|
|
||||||
<ListBox
|
<ListBox
|
||||||
AutoScrollToSelectedItem="True"
|
AutoScrollToSelectedItem="True"
|
||||||
Classes="ContentListView"
|
Classes="ContentListView"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
IsTabStop="True"
|
IsTabStop="True"
|
||||||
ItemsSource="{Binding AppState.SelectedTab^.CurrentItemsCollectionObservable^}"
|
ItemsSource="{Binding AppState.SelectedTab^.CurrentItems.Value}"
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||||
SelectedItem="{Binding AppState.SelectedTab^.CurrentSelectedItem^}"
|
SelectedItem="{Binding AppState.SelectedTab^.CurrentSelectedItem.Value}"
|
||||||
x:Name="CurrentItems">
|
x:Name="CurrentItems">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="corevm:IItemViewModel">
|
<DataTemplate x:DataType="corevm:IItemViewModel">
|
||||||
@@ -439,9 +432,8 @@
|
|||||||
Foreground="{DynamicResource ErrorBrush}"
|
Foreground="{DynamicResource ErrorBrush}"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
IsVisible="{Binding AppState.SelectedTab^.CurrentItemsCollection.Collection.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"
|
IsVisible="{Binding AppState.SelectedTab^.CurrentItems.Value.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"
|
||||||
Margin="10"
|
Margin="10"
|
||||||
x:CompileBindings="False"
|
|
||||||
x:Name="CurrentEmpty">
|
x:Name="CurrentEmpty">
|
||||||
Empty
|
Empty
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
@@ -457,13 +449,35 @@
|
|||||||
|
|
||||||
<Grid Grid.Column="4">
|
<Grid Grid.Column="4">
|
||||||
<Grid IsVisible="{Binding ItemPreviewService.ItemPreview^, Converter={x:Static ObjectConverters.IsNull}}">
|
<Grid IsVisible="{Binding ItemPreviewService.ItemPreview^, Converter={x:Static ObjectConverters.IsNull}}">
|
||||||
<Grid IsVisible="{Binding AppState.SelectedTab^.SelectedsChildrenCollection.Collection, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}">
|
<Grid IsVisible="{Binding AppState.SelectedTab^.SelectedsChildren.Value, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}">
|
||||||
|
<Grid RowDefinitions="Auto, Auto, *">
|
||||||
|
|
||||||
|
<Grid IsVisible="{Binding AppState.SelectedTab^.CurrentSelectedItemAsContainer.Value.Container.IsLoading^, FallbackValue=False}">
|
||||||
|
<Image
|
||||||
|
Classes="LoadingAnimation"
|
||||||
|
Height="40"
|
||||||
|
Source="{SvgImage /Assets/loading.svg}"
|
||||||
|
Width="40" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemsRepeater Grid.Row="1" ItemsSource="{Binding AppState.SelectedTab^.CurrentLocation.Value.Exceptions}">
|
||||||
|
<ItemsRepeater.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock
|
||||||
|
Classes="ErrorText"
|
||||||
|
Margin="5,0,5,10"
|
||||||
|
Text="{Binding Converter={StaticResource ExceptionToStringConverter}}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsRepeater.ItemTemplate>
|
||||||
|
</ItemsRepeater>
|
||||||
|
|
||||||
<ListBox
|
<ListBox
|
||||||
AutoScrollToSelectedItem="True"
|
AutoScrollToSelectedItem="True"
|
||||||
Classes="ContentListView"
|
Classes="ContentListView"
|
||||||
IsVisible="{Binding AppState.SelectedTab^.SelectedsChildrenCollection.Collection.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}"
|
Grid.Row="2"
|
||||||
ItemsSource="{Binding AppState.SelectedTab^.SelectedsChildrenCollection.Collection}"
|
IsVisible="{Binding AppState.SelectedTab^.SelectedsChildren.Value.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}"
|
||||||
x:CompileBindings="False"
|
ItemsSource="{Binding AppState.SelectedTab^.SelectedsChildren.Value}"
|
||||||
x:Name="ChildItems">
|
x:Name="ChildItems">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="corevm:IItemViewModel">
|
<DataTemplate x:DataType="corevm:IItemViewModel">
|
||||||
@@ -471,20 +485,20 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
Foreground="{DynamicResource ErrorBrush}"
|
Foreground="{DynamicResource ErrorBrush}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
IsVisible="{Binding AppState.SelectedTab^.SelectedsChildrenCollection.Collection.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"
|
IsVisible="{Binding AppState.SelectedTab^.SelectedsChildren.Value.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"
|
||||||
Margin="10"
|
Margin="10"
|
||||||
x:CompileBindings="False"
|
|
||||||
x:Name="ChildEmpty">
|
x:Name="ChildEmpty">
|
||||||
Empty
|
Empty
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid IsVisible="{Binding AppState.SelectedTab^.SelectedsChildrenCollection, Converter={x:Static ObjectConverters.IsNull}, ConverterParameter=0, FallbackValue=False}" RowDefinitions="Auto, Auto">
|
<Grid IsVisible="{Binding AppState.SelectedTab^.SelectedsChildren.Value, Converter={x:Static ObjectConverters.IsNull}, ConverterParameter=0, FallbackValue=False}" RowDefinitions="Auto, Auto">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Foreground="{DynamicResource ErrorBrush}"
|
Foreground="{DynamicResource ErrorBrush}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
@@ -492,10 +506,13 @@
|
|||||||
Text="There were some errors while opening container."
|
Text="There were some errors while opening container."
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
<ItemsRepeater Grid.Row="1" ItemsSource="{Binding AppState.SelectedTab^.CurrentSelectedItem^.BaseItem.Exceptions^}">
|
<ItemsRepeater Grid.Row="1" ItemsSource="{Binding AppState.SelectedTab^.CurrentSelectedItem.Value.BaseItem.Exceptions}">
|
||||||
<ItemsRepeater.ItemTemplate>
|
<ItemsRepeater.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Margin="5,0,5,10" Text="{Binding Converter={StaticResource ExceptionToStringConverter}}" />
|
<TextBlock
|
||||||
|
Classes="ErrorText"
|
||||||
|
Margin="5,0,5,10"
|
||||||
|
Text="{Binding Converter={StaticResource ExceptionToStringConverter}}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsRepeater.ItemTemplate>
|
</ItemsRepeater.ItemTemplate>
|
||||||
</ItemsRepeater>
|
</ItemsRepeater>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
|
|
||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public class CollectionRepeaterProperty<TCollection, TItem> : DeclarativePropertyBase<TCollection>
|
||||||
|
where TCollection : IList<TItem>, INotifyCollectionChanged
|
||||||
|
{
|
||||||
|
private TCollection? _currentCollection;
|
||||||
|
|
||||||
|
public CollectionRepeaterProperty(IDeclarativeProperty<TCollection?> from)
|
||||||
|
{
|
||||||
|
_currentCollection = from.Value;
|
||||||
|
if (from.Value is { } value)
|
||||||
|
{
|
||||||
|
value.CollectionChanged += HandleCollectionChanged;
|
||||||
|
}
|
||||||
|
AddDisposable(from.Subscribe(Handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var t = Task.Run(async () => await NotifySubscribersAsync(Value));
|
||||||
|
t.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Handle(TCollection? collection, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (_currentCollection is { } currentCollection)
|
||||||
|
{
|
||||||
|
currentCollection.CollectionChanged -= HandleCollectionChanged;
|
||||||
|
}
|
||||||
|
if (collection is {} newCollection)
|
||||||
|
{
|
||||||
|
//newCollection.CollectionChanged -= HandleCollectionChanged;
|
||||||
|
newCollection.CollectionChanged += HandleCollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentCollection = collection;
|
||||||
|
|
||||||
|
await SetNewValueAsync(collection, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/Library/DeclarativeProperty/CombineLatestProperty.cs
Normal file
36
src/Library/DeclarativeProperty/CombineLatestProperty.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public sealed class CombineLatestProperty<T1, T2, TResult> : DeclarativePropertyBase<TResult>
|
||||||
|
{
|
||||||
|
private readonly Func<T1?, T2?, Task<TResult>> _func;
|
||||||
|
private T1? _value1;
|
||||||
|
private T2? _value2;
|
||||||
|
|
||||||
|
public CombineLatestProperty(IDeclarativeProperty<T1> prop1, IDeclarativeProperty<T2> prop2, Func<T1?, T2?, Task<TResult>> func)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(prop1);
|
||||||
|
ArgumentNullException.ThrowIfNull(prop2);
|
||||||
|
|
||||||
|
_func = func;
|
||||||
|
|
||||||
|
_value1 = prop1.Value is null ? default : prop1.Value;
|
||||||
|
_value2 = prop2.Value is null ? default : prop2.Value;
|
||||||
|
|
||||||
|
prop1.Subscribe(async (value1, token) =>
|
||||||
|
{
|
||||||
|
_value1 = value1;
|
||||||
|
await UpdateAsync(token);
|
||||||
|
});
|
||||||
|
prop2.Subscribe(async (value2, token) =>
|
||||||
|
{
|
||||||
|
_value2 = value2;
|
||||||
|
await UpdateAsync(token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var result = await _func(_value1, _value2);
|
||||||
|
await SetNewValueAsync(result, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/Library/DeclarativeProperty/DebounceProperty.cs
Normal file
52
src/Library/DeclarativeProperty/DebounceProperty.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public sealed class DebounceProperty<T> : TimingPropertyBase<T>
|
||||||
|
{
|
||||||
|
private CancellationTokenSource? _debounceCts;
|
||||||
|
private bool _isActive;
|
||||||
|
private DateTime _startTime;
|
||||||
|
public bool ResetTimer { get; init; }
|
||||||
|
public TimeSpan WaitInterval { get; init; } = TimeSpan.FromMilliseconds(1);
|
||||||
|
|
||||||
|
public DebounceProperty(
|
||||||
|
IDeclarativeProperty<T> from,
|
||||||
|
TimeSpan interval,
|
||||||
|
Action<T?>? setValueHook = null) : base(from, interval, setValueHook)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task SetValue(T? next, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_debounceCts?.Cancel();
|
||||||
|
var newTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
_debounceCts = newTokenSource;
|
||||||
|
|
||||||
|
var newToken = newTokenSource.Token;
|
||||||
|
|
||||||
|
if (!_isActive || ResetTimer)
|
||||||
|
{
|
||||||
|
_isActive = true;
|
||||||
|
_startTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (DateTime.Now - _startTime < Interval)
|
||||||
|
{
|
||||||
|
await Task.Delay(WaitInterval, newToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
WithLock(() => { _isActive = false; });
|
||||||
|
|
||||||
|
await FireAsync(next, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/Library/DeclarativeProperty/DeclarativeProperty.cs
Normal file
25
src/Library/DeclarativeProperty/DeclarativeProperty.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public static class DeclarativePropertyHelpers
|
||||||
|
{
|
||||||
|
public static CombineLatestProperty<T1, T2, TResult> CombineLatest<T1, T2, TResult>(
|
||||||
|
IDeclarativeProperty<T1> prop1,
|
||||||
|
IDeclarativeProperty<T2> prop2,
|
||||||
|
Func<T1, T2, Task<TResult>> func)
|
||||||
|
=> new(prop1, prop2, func);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DeclarativeProperty<T> : DeclarativePropertyBase<T>
|
||||||
|
{
|
||||||
|
public DeclarativeProperty(Action<T?>? setValueHook = null) : base(setValueHook)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeclarativeProperty(T initialValue, Action<T?>? setValueHook = null) : base(initialValue, setValueHook)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetValue(T newValue, CancellationToken cancellationToken = default)
|
||||||
|
=> await SetNewValueAsync(newValue, cancellationToken);
|
||||||
|
}
|
||||||
13
src/Library/DeclarativeProperty/DeclarativeProperty.csproj
Normal file
13
src/Library/DeclarativeProperty/DeclarativeProperty.csproj
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ObservableComputations" Version="2.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
143
src/Library/DeclarativeProperty/DeclarativePropertyBase.cs
Normal file
143
src/Library/DeclarativeProperty/DeclarativePropertyBase.cs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
|
||||||
|
{
|
||||||
|
private readonly List<Func<T?, CancellationToken, Task>> _subscribers = new();
|
||||||
|
private readonly Action<T?>? _setValueHook;
|
||||||
|
private readonly List<IDisposable> _disposables = new();
|
||||||
|
private readonly List<Func<IDeclarativeProperty<T>, T, IDisposable?>> _subscribeTriggers = new();
|
||||||
|
private readonly List<Action<IDeclarativeProperty<T>, T>> _unsubscribeTriggers = new();
|
||||||
|
private readonly List<IDisposable> _triggerDisposables = new();
|
||||||
|
private readonly object _triggerLock = new();
|
||||||
|
|
||||||
|
private T? _value;
|
||||||
|
|
||||||
|
public T? Value
|
||||||
|
{
|
||||||
|
get => _value;
|
||||||
|
set => _setValueHook?.Invoke(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DeclarativePropertyBase(Action<T?>? setValueHook = null)
|
||||||
|
{
|
||||||
|
_setValueHook = setValueHook;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DeclarativePropertyBase(T? initialValue, Action<T?>? setValueHook = null)
|
||||||
|
{
|
||||||
|
_setValueHook = setValueHook;
|
||||||
|
_value = initialValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected async Task NotifySubscribersAsync(T? newValue, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var subscribers = _subscribers.ToList();
|
||||||
|
foreach (var handler in subscribers)
|
||||||
|
{
|
||||||
|
await handler(newValue, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
|
||||||
|
public IDisposable Subscribe(Func<T?, CancellationToken, Task> onChange)
|
||||||
|
{
|
||||||
|
_subscribers.Add(onChange);
|
||||||
|
onChange(_value, default);
|
||||||
|
|
||||||
|
return new Unsubscriber<T>(this, onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unsubscribe(Func<T, CancellationToken, Task> onChange) => _subscribers.Remove(onChange);
|
||||||
|
|
||||||
|
public IDeclarativeProperty<T> RegisterTrigger(
|
||||||
|
Func<IDeclarativeProperty<T>, T, IDisposable?> triggerSubscribe,
|
||||||
|
Action<IDeclarativeProperty<T>, T>? triggerUnsubscribe = null)
|
||||||
|
{
|
||||||
|
lock (_triggerLock)
|
||||||
|
{
|
||||||
|
if (Value != null)
|
||||||
|
{
|
||||||
|
var disposable = triggerSubscribe(this, Value);
|
||||||
|
if (disposable != null) _triggerDisposables.Add(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
_subscribeTriggers.Add(triggerSubscribe);
|
||||||
|
if (triggerUnsubscribe != null) _unsubscribeTriggers.Add(triggerUnsubscribe);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IDisposable IObservable<T>.Subscribe(IObserver<T> observer)
|
||||||
|
=> Subscribe((v, _) =>
|
||||||
|
{
|
||||||
|
observer.OnNext(v);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async Task SetNewValueAsync(T? newValue, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!(Value?.Equals(newValue) ?? false))
|
||||||
|
{
|
||||||
|
lock (_triggerLock)
|
||||||
|
{
|
||||||
|
if (_value != null)
|
||||||
|
{
|
||||||
|
foreach (var unsubscribeTrigger in _unsubscribeTriggers)
|
||||||
|
{
|
||||||
|
unsubscribeTrigger(this, _value);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var triggerDisposable in _triggerDisposables)
|
||||||
|
{
|
||||||
|
triggerDisposable.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_triggerDisposables.Clear();
|
||||||
|
|
||||||
|
if(cancellationToken.IsCancellationRequested) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_value = newValue;
|
||||||
|
|
||||||
|
if (_value != null)
|
||||||
|
{
|
||||||
|
foreach (var subscribeTrigger in _subscribeTriggers)
|
||||||
|
{
|
||||||
|
if(cancellationToken.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
var disposable = subscribeTrigger(this, _value);
|
||||||
|
if (disposable != null) _triggerDisposables.Add(disposable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
await NotifySubscribersAsync(newValue, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReFireAsync()
|
||||||
|
=> await SetNewValueAsync(Value);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var disposable in _disposables)
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_subscribers.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddDisposable(IDisposable disposable) => _disposables.Add(disposable);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using ObservableComputations;
|
||||||
|
|
||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public static class DeclarativePropertyExtensions
|
||||||
|
{
|
||||||
|
public static IDeclarativeProperty<T> Debounce<T>(this IDeclarativeProperty<T> from, TimeSpan interval, bool resetTimer = false)
|
||||||
|
=> new DebounceProperty<T>(from, interval){ResetTimer = resetTimer};
|
||||||
|
|
||||||
|
public static IDeclarativeProperty<T> DistinctUntilChanged<T>(this IDeclarativeProperty<T> from)
|
||||||
|
=> new DistinctUntilChangedProperty<T>(from);
|
||||||
|
|
||||||
|
public static IDeclarativeProperty<TTo?> Map<TFrom, TTo>(this IDeclarativeProperty<TFrom?> from, Func<TFrom?, CancellationToken, Task<TTo?>> mapper)
|
||||||
|
=> new MapProperty<TFrom?, TTo?>(mapper, from);
|
||||||
|
|
||||||
|
public static IDeclarativeProperty<TTo?> Map<TFrom, TTo>(this IDeclarativeProperty<TFrom?> from, Func<TFrom?, TTo?> mapper)
|
||||||
|
=> new MapProperty<TFrom?, TTo?>((next, _) => Task.FromResult(mapper(next)), from);
|
||||||
|
|
||||||
|
public static async Task<IDeclarativeProperty<TTo?>> MapAsync<TFrom, TTo>(this IDeclarativeProperty<TFrom?> from, Func<TFrom?, CancellationToken, Task<TTo?>> mapper)
|
||||||
|
=> await MapProperty<TFrom?, TTo?>.CreateAsync(mapper, from);
|
||||||
|
|
||||||
|
public static async Task<IDeclarativeProperty<TTo?>> MapAsync<TFrom, TTo>(this IDeclarativeProperty<TFrom?> from, Func<TFrom?, TTo?> mapper)
|
||||||
|
=> await MapProperty<TFrom?, TTo?>.CreateAsync((next, _) => Task.FromResult(mapper(next)), from);
|
||||||
|
|
||||||
|
public static IDisposable Subscribe<T>(this IDeclarativeProperty<T> property, Action<T?, CancellationToken> onChange)
|
||||||
|
=> property.Subscribe((value, token) =>
|
||||||
|
{
|
||||||
|
onChange(value, token);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
public static IDisposable Subscribe<T>(this IDeclarativeProperty<T> property, Action<T?> onChange)
|
||||||
|
=> property.Subscribe((value, _) =>
|
||||||
|
{
|
||||||
|
onChange(value);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
public static IDeclarativeProperty<T> Extract<T>(
|
||||||
|
this IDeclarativeProperty<ReadOnlyObservableCollection<T>> from,
|
||||||
|
Func<IList<T>?, T?> extractor
|
||||||
|
)
|
||||||
|
=> new ExtractorProperty<T>(from, extractor);
|
||||||
|
|
||||||
|
public static IDeclarativeProperty<T> Extract<T>(
|
||||||
|
this IDeclarativeProperty<ObservableCollection<T>> from,
|
||||||
|
Func<IList<T>?, T?> extractor
|
||||||
|
)
|
||||||
|
=> new ExtractorProperty<T>(from, extractor);
|
||||||
|
|
||||||
|
public static IDeclarativeProperty<T> Extract<T>(
|
||||||
|
this IDeclarativeProperty<CollectionComputing<T>> from,
|
||||||
|
Func<IList<T>?, T?> extractor
|
||||||
|
)
|
||||||
|
=> new ExtractorProperty<T>(from, extractor);
|
||||||
|
|
||||||
|
public static IDeclarativeProperty<TCollection?> Watch<TCollection, TItem>(
|
||||||
|
this IDeclarativeProperty<TCollection?> collection)
|
||||||
|
where TCollection : IList<TItem>, INotifyCollectionChanged
|
||||||
|
=> new CollectionRepeaterProperty<TCollection?, TItem>(collection);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public sealed class DistinctUntilChangedProperty<T> : DeclarativePropertyBase<T>
|
||||||
|
{
|
||||||
|
private readonly Func<T?, T?, bool>? _comparer;
|
||||||
|
private bool _firstFire = true;
|
||||||
|
|
||||||
|
public DistinctUntilChangedProperty(IDeclarativeProperty<T> from, Func<T?, T?, bool>? comparer = null) : base(from.Value)
|
||||||
|
{
|
||||||
|
_comparer = comparer;
|
||||||
|
AddDisposable(from.Subscribe(Handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task Handle(T? next, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (_comparer is { } comparer)
|
||||||
|
{
|
||||||
|
if (comparer(Value, next))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((next is null && Value is null && !_firstFire)
|
||||||
|
|| (Value?.Equals(next) ?? false))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_firstFire = false;
|
||||||
|
await SetNewValueAsync(next, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
174
src/Library/DeclarativeProperty/ExtractorProperty.cs
Normal file
174
src/Library/DeclarativeProperty/ExtractorProperty.cs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using ObservableComputations;
|
||||||
|
|
||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public sealed class ExtractorProperty<T> : DeclarativePropertyBase<T>
|
||||||
|
{
|
||||||
|
private interface ICollectionWrapper : IDisposable
|
||||||
|
{
|
||||||
|
IList<T> Collection { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ICollectionWrapper<in TCollection> : ICollectionWrapper
|
||||||
|
where TCollection : IList<T>, INotifyCollectionChanged
|
||||||
|
{
|
||||||
|
static abstract ICollectionWrapper<TCollection> Create(
|
||||||
|
TCollection collection,
|
||||||
|
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObservableCollectionWrapper : ICollectionWrapper<ObservableCollection<T>>
|
||||||
|
{
|
||||||
|
private readonly NotifyCollectionChangedEventHandler _handler;
|
||||||
|
private readonly ObservableCollection<T> _collection;
|
||||||
|
|
||||||
|
public ObservableCollectionWrapper(
|
||||||
|
ObservableCollection<T> collection,
|
||||||
|
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_collection = collection;
|
||||||
|
_handler = new NotifyCollectionChangedEventHandler(handler);
|
||||||
|
|
||||||
|
_collection.CollectionChanged += _handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<T> Collection => _collection;
|
||||||
|
|
||||||
|
public void Dispose() => _collection.CollectionChanged -= _handler;
|
||||||
|
|
||||||
|
|
||||||
|
public static ICollectionWrapper<ObservableCollection<T>> Create(
|
||||||
|
ObservableCollection<T> collection,
|
||||||
|
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||||
|
) => new ObservableCollectionWrapper(collection, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ReadOnlyObservableCollectionWrapper : ICollectionWrapper<ReadOnlyObservableCollection<T>>
|
||||||
|
{
|
||||||
|
private readonly NotifyCollectionChangedEventHandler _handler;
|
||||||
|
private readonly ReadOnlyObservableCollection<T> _collection;
|
||||||
|
|
||||||
|
public ReadOnlyObservableCollectionWrapper(
|
||||||
|
ReadOnlyObservableCollection<T> collection,
|
||||||
|
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_collection = collection;
|
||||||
|
_handler = new NotifyCollectionChangedEventHandler(handler);
|
||||||
|
|
||||||
|
((INotifyCollectionChanged) _collection).CollectionChanged += _handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<T> Collection => _collection;
|
||||||
|
|
||||||
|
public void Dispose() => ((INotifyCollectionChanged) _collection).CollectionChanged -= _handler;
|
||||||
|
|
||||||
|
|
||||||
|
public static ICollectionWrapper<ReadOnlyObservableCollection<T>> Create(
|
||||||
|
ReadOnlyObservableCollection<T> collection,
|
||||||
|
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||||
|
) => new ReadOnlyObservableCollectionWrapper(collection, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CollectionComputingWrapper : ICollectionWrapper<CollectionComputing<T>>
|
||||||
|
{
|
||||||
|
private readonly OcConsumer _consumer;
|
||||||
|
private readonly NotifyCollectionChangedEventHandler _handler;
|
||||||
|
private readonly CollectionComputing<T> _collection;
|
||||||
|
|
||||||
|
public CollectionComputingWrapper(
|
||||||
|
CollectionComputing<T> collection,
|
||||||
|
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_collection = collection;
|
||||||
|
_handler = new NotifyCollectionChangedEventHandler(handler);
|
||||||
|
|
||||||
|
((INotifyCollectionChanged) _collection).CollectionChanged += _handler;
|
||||||
|
|
||||||
|
collection.For(_consumer = new OcConsumer());
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<T> Collection => _collection;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
((INotifyCollectionChanged) _collection).CollectionChanged -= _handler;
|
||||||
|
_consumer.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static ICollectionWrapper<CollectionComputing<T>> Create(
|
||||||
|
CollectionComputing<T> collection,
|
||||||
|
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||||
|
) => new CollectionComputingWrapper(collection, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Func<IList<T>?, T?> _extractor;
|
||||||
|
private ICollectionWrapper? _collectionWrapper;
|
||||||
|
|
||||||
|
public ExtractorProperty(
|
||||||
|
IDeclarativeProperty<ObservableCollection<T>> from,
|
||||||
|
Func<IList<T>?, T?> extractor) : base(extractor(from.Value))
|
||||||
|
{
|
||||||
|
_extractor = extractor;
|
||||||
|
_collectionWrapper = from.Value is null
|
||||||
|
? null
|
||||||
|
: new ObservableCollectionWrapper(from.Value, CollectionUpdated);
|
||||||
|
|
||||||
|
AddDisposable(from.Subscribe(SetValue<ObservableCollectionWrapper, ObservableCollection<T>>));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtractorProperty(
|
||||||
|
IDeclarativeProperty<ReadOnlyObservableCollection<T>> from,
|
||||||
|
Func<IList<T>?, T?> extractor) : base(extractor(from.Value))
|
||||||
|
{
|
||||||
|
_extractor = extractor;
|
||||||
|
_collectionWrapper = from.Value is null
|
||||||
|
? null
|
||||||
|
: new ReadOnlyObservableCollectionWrapper(from.Value, CollectionUpdated);
|
||||||
|
|
||||||
|
AddDisposable(from.Subscribe(SetValue<ReadOnlyObservableCollectionWrapper, ReadOnlyObservableCollection<T>>));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtractorProperty(
|
||||||
|
IDeclarativeProperty<CollectionComputing<T>> from,
|
||||||
|
Func<IList<T>?, T?> extractor) : base(extractor(from.Value))
|
||||||
|
{
|
||||||
|
_extractor = extractor;
|
||||||
|
_collectionWrapper = from.Value is null
|
||||||
|
? null
|
||||||
|
: new CollectionComputingWrapper(from.Value, CollectionUpdated);
|
||||||
|
|
||||||
|
AddDisposable(from.Subscribe(SetValue<CollectionComputingWrapper, CollectionComputing<T>>));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CollectionUpdated(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
=> Task.Run(async () => await Fire(_collectionWrapper?.Collection)).Wait();
|
||||||
|
|
||||||
|
private async Task SetValue<TWrapper, TCollection>(TCollection? next, CancellationToken cancellationToken = default)
|
||||||
|
where TCollection : IList<T>, INotifyCollectionChanged
|
||||||
|
where TWrapper : ICollectionWrapper<TCollection>
|
||||||
|
{
|
||||||
|
_collectionWrapper?.Dispose();
|
||||||
|
|
||||||
|
_collectionWrapper = next is null
|
||||||
|
? null
|
||||||
|
: TWrapper.Create(next, CollectionUpdated);
|
||||||
|
|
||||||
|
await Fire(next, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Fire(IList<T>? items, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var newValue = items is null
|
||||||
|
? default
|
||||||
|
: _extractor(items);
|
||||||
|
|
||||||
|
await SetNewValueAsync(newValue, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/Library/DeclarativeProperty/FilterProperty.cs
Normal file
34
src/Library/DeclarativeProperty/FilterProperty.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public sealed class FilterProperty<T> : DeclarativePropertyBase<T>
|
||||||
|
{
|
||||||
|
private readonly Func<T?, Task<bool>> _filter;
|
||||||
|
|
||||||
|
public FilterProperty(
|
||||||
|
Func<T?, Task<bool>> filter,
|
||||||
|
IDeclarativeProperty<T> from,
|
||||||
|
Action<T?>? setValueHook = null) : base(setValueHook)
|
||||||
|
{
|
||||||
|
_filter = filter;
|
||||||
|
AddDisposable(from.Subscribe(SetValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetValue(T? next, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (await _filter(next))
|
||||||
|
{
|
||||||
|
await SetNewValueAsync(next, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<FilterProperty<T>> CreateAsync(
|
||||||
|
Func<T?, Task<bool>> filter,
|
||||||
|
IDeclarativeProperty<T> from,
|
||||||
|
Action<T?>? setValueHook = null)
|
||||||
|
{
|
||||||
|
var prop = new FilterProperty<T>(filter, from, setValueHook);
|
||||||
|
await prop.SetValue(from.Value);
|
||||||
|
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/Library/DeclarativeProperty/IDeclarativeProperty.cs
Normal file
17
src/Library/DeclarativeProperty/IDeclarativeProperty.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public interface IDeclarativeProperty<T> : INotifyPropertyChanged, IDisposable, IObservable<T>
|
||||||
|
{
|
||||||
|
T? Value { get; set; }
|
||||||
|
IDisposable Subscribe(Func<T?, CancellationToken, Task> onChange);
|
||||||
|
void Unsubscribe(Func<T?, CancellationToken, Task> onChange);
|
||||||
|
|
||||||
|
IDeclarativeProperty<T> RegisterTrigger(
|
||||||
|
Func<IDeclarativeProperty<T>, T, IDisposable?> triggerSubscribe,
|
||||||
|
Action<IDeclarativeProperty<T>, T>? triggerUnsubscribe = null
|
||||||
|
);
|
||||||
|
|
||||||
|
Task ReFireAsync();
|
||||||
|
}
|
||||||
32
src/Library/DeclarativeProperty/MapProperty.cs
Normal file
32
src/Library/DeclarativeProperty/MapProperty.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public sealed class MapProperty<TFrom, TTo> : DeclarativePropertyBase<TTo>
|
||||||
|
{
|
||||||
|
private readonly Func<TFrom?, CancellationToken, Task<TTo?>> _mapper;
|
||||||
|
|
||||||
|
public MapProperty(
|
||||||
|
Func<TFrom?, CancellationToken, Task<TTo?>> mapper,
|
||||||
|
IDeclarativeProperty<TFrom?> from,
|
||||||
|
Action<TTo?>? setValueHook = null) : base(setValueHook)
|
||||||
|
{
|
||||||
|
_mapper = mapper;
|
||||||
|
AddDisposable(from.Subscribe(SetValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetValue(TFrom? next, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var newValue = await _mapper(next, cancellationToken);
|
||||||
|
await SetNewValueAsync(newValue, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<MapProperty<TFrom?, TTo?>> CreateAsync(
|
||||||
|
Func<TFrom?, CancellationToken, Task<TTo?>> mapper,
|
||||||
|
IDeclarativeProperty<TFrom?> from,
|
||||||
|
Action<TTo?>? setValueHook = null)
|
||||||
|
{
|
||||||
|
var prop = new MapProperty<TFrom?, TTo?>(mapper, from, setValueHook);
|
||||||
|
await prop.SetValue(from.Value);
|
||||||
|
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Library/DeclarativeProperty/SwitchProperty.cs
Normal file
24
src/Library/DeclarativeProperty/SwitchProperty.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public sealed class SwitchProperty<TItem> : DeclarativePropertyBase<TItem>
|
||||||
|
{
|
||||||
|
private IDisposable? _innerSubscription;
|
||||||
|
|
||||||
|
public SwitchProperty(IDeclarativeProperty<IDeclarativeProperty<TItem?>?> from) : base(from.Value is null ? default : from.Value.Value)
|
||||||
|
{
|
||||||
|
AddDisposable(from.Subscribe(HandleStreamChange));
|
||||||
|
_innerSubscription = from.Value?.Subscribe(HandleInnerValueChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleStreamChange(IDeclarativeProperty<TItem?>? next, CancellationToken token)
|
||||||
|
{
|
||||||
|
_innerSubscription?.Dispose();
|
||||||
|
_innerSubscription = next?.Subscribe(HandleInnerValueChange);
|
||||||
|
|
||||||
|
|
||||||
|
await SetNewValueAsync(next is null ? default : next.Value, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleInnerValueChange(TItem? next, CancellationToken token)
|
||||||
|
=> await SetNewValueAsync(next, token);
|
||||||
|
}
|
||||||
58
src/Library/DeclarativeProperty/ThrottleProperty.cs
Normal file
58
src/Library/DeclarativeProperty/ThrottleProperty.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public class ThrottleProperty<T> : TimingPropertyBase<T>
|
||||||
|
{
|
||||||
|
private CancellationTokenSource? _debounceCts;
|
||||||
|
private DateTime _lastFired;
|
||||||
|
|
||||||
|
public ThrottleProperty(
|
||||||
|
IDeclarativeProperty<T> from,
|
||||||
|
TimeSpan interval,
|
||||||
|
Action<T?>? setValueHook = null) : base(from, interval, setValueHook)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task SetValue(T? next, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_debounceCts?.Cancel();
|
||||||
|
if (DateTime.Now - _lastFired > Interval)
|
||||||
|
{
|
||||||
|
_lastFired = DateTime.Now;
|
||||||
|
// Note: Recursive chains can happen. Awaiting this can cause a deadlock.
|
||||||
|
Task.Run(async () => await FireAsync(next, cancellationToken));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_debounceCts = new();
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(Interval, _debounceCts.Token);
|
||||||
|
await FireIfNeededAsync(
|
||||||
|
next,
|
||||||
|
() => { _lastFired = DateTime.Now; },
|
||||||
|
_debounceCts.Token, cancellationToken
|
||||||
|
);
|
||||||
|
/*var shouldFire = WithLock(() =>
|
||||||
|
{
|
||||||
|
if (_debounceCts.Token.IsCancellationRequested)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_lastFired = DateTime.Now;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shouldFire) return;
|
||||||
|
|
||||||
|
await Fire(next, cancellationToken);*/
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/Library/DeclarativeProperty/TimingPropertyBase.cs
Normal file
85
src/Library/DeclarativeProperty/TimingPropertyBase.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
public abstract class TimingPropertyBase<T> : DeclarativePropertyBase<T>
|
||||||
|
{
|
||||||
|
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||||
|
protected TimeSpan Interval { get; }
|
||||||
|
|
||||||
|
protected TimingPropertyBase(
|
||||||
|
IDeclarativeProperty<T> from,
|
||||||
|
TimeSpan interval,
|
||||||
|
Action<T?>? setValueHook = null) : base(from.Value, setValueHook)
|
||||||
|
{
|
||||||
|
Interval = interval;
|
||||||
|
AddDisposable(from.Subscribe(SetValueInternal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetValueInternal(T? next, CancellationToken cancellationToken = default)
|
||||||
|
=> await WithLockAsync(async () => await SetValue(next, cancellationToken), cancellationToken);
|
||||||
|
|
||||||
|
|
||||||
|
protected void WithLock(Action action)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_semaphore.Wait();
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TResult WithLock<TResult>(Func<TResult> func)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_semaphore.Wait();
|
||||||
|
return func();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task WithLockAsync(Func<Task> action, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _semaphore.WaitAsync(cancellationToken);
|
||||||
|
await action();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task SetValue(T? next, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
protected async Task FireIfNeededAsync(
|
||||||
|
T? next,
|
||||||
|
Action cleanup,
|
||||||
|
CancellationToken timingCancellationToken = default,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await Task.Delay(Interval, timingCancellationToken);
|
||||||
|
var shouldFire = WithLock(() =>
|
||||||
|
{
|
||||||
|
if (timingCancellationToken.IsCancellationRequested)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shouldFire) return;
|
||||||
|
|
||||||
|
await FireAsync(next, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task FireAsync(T? next, CancellationToken cancellationToken = default)
|
||||||
|
=> await SetNewValueAsync(next, cancellationToken);
|
||||||
|
}
|
||||||
14
src/Library/DeclarativeProperty/Unsubscriber.cs
Normal file
14
src/Library/DeclarativeProperty/Unsubscriber.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace DeclarativeProperty;
|
||||||
|
|
||||||
|
internal sealed class Unsubscriber<T> : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IDeclarativeProperty<T> _owner;
|
||||||
|
private readonly Func<T?, CancellationToken, Task> _onChange;
|
||||||
|
|
||||||
|
public Unsubscriber(IDeclarativeProperty<T> owner, Func<T?, CancellationToken, Task> onChange)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_onChange = onChange;
|
||||||
|
}
|
||||||
|
public void Dispose() => _owner.Unsubscribe(_onChange);
|
||||||
|
}
|
||||||
13
src/Library/Defer/Defer.cs
Normal file
13
src/Library/Defer/Defer.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace System;
|
||||||
|
|
||||||
|
public static class DeferTools
|
||||||
|
{
|
||||||
|
public static IDisposable Defer(Action action) => new DeferDisposable(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly struct DeferDisposable : IDisposable
|
||||||
|
{
|
||||||
|
readonly Action _action;
|
||||||
|
public DeferDisposable(Action action) => _action = action;
|
||||||
|
public void Dispose() => _action();
|
||||||
|
}
|
||||||
10
src/Library/Defer/Defer.csproj
Normal file
10
src/Library/Defer/Defer.csproj
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>System</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using FileTime.Core.ContentAccess;
|
using FileTime.Core.ContentAccess;
|
||||||
@@ -36,17 +37,19 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
|
|||||||
? new DirectoryInfo("/").GetDirectories()
|
? new DirectoryInfo("/").GetDirectories()
|
||||||
: Environment.GetLogicalDrives().Select(d => new DirectoryInfo(d));
|
: Environment.GetLogicalDrives().Select(d => new DirectoryInfo(d));
|
||||||
|
|
||||||
Items.Edit(actions =>
|
Items.Clear();
|
||||||
|
Items.AddRange(rootDirectories.Select(d => DirectoryToAbsolutePath(d, PointInTime.Present)));
|
||||||
|
|
||||||
|
/*Items.Edit(actions =>
|
||||||
{
|
{
|
||||||
actions.Clear();
|
actions.Clear();
|
||||||
actions.AddOrUpdate(rootDirectories.Select(d => DirectoryToAbsolutePath(d, PointInTime.Present)));
|
actions.AddOrUpdate(rootDirectories.Select(d => DirectoryToAbsolutePath(d, PointInTime.Present)));
|
||||||
});
|
});*/
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanHandlePath(NativePath path)
|
public override bool CanHandlePath(NativePath path)
|
||||||
{
|
{
|
||||||
var rootDrive = Items
|
var rootDrive = Items
|
||||||
.Items
|
|
||||||
.FirstOrDefault(r =>
|
.FirstOrDefault(r =>
|
||||||
path.Path.StartsWith(
|
path.Path.StartsWith(
|
||||||
GetNativePath(r.Path).Path,
|
GetNativePath(r.Path).Path,
|
||||||
@@ -132,7 +135,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
|
|||||||
PointInTime pointInTime,
|
PointInTime pointInTime,
|
||||||
IEnumerable<Exception>? initialExceptions = null)
|
IEnumerable<Exception>? initialExceptions = null)
|
||||||
{
|
{
|
||||||
var exceptions = new SourceList<Exception>();
|
var exceptions = new ObservableCollection<Exception>();
|
||||||
if (initialExceptions is not null)
|
if (initialExceptions is not null)
|
||||||
{
|
{
|
||||||
exceptions.AddRange(initialExceptions);
|
exceptions.AddRange(initialExceptions);
|
||||||
@@ -166,9 +169,9 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
|
|||||||
this,
|
this,
|
||||||
true,
|
true,
|
||||||
pointInTime,
|
pointInTime,
|
||||||
exceptions.Connect(),
|
exceptions,
|
||||||
new ExtensionCollection().AsReadOnly(),
|
new ExtensionCollection().AsReadOnly(),
|
||||||
new SourceCache<AbsolutePath, string>(a => a.Path.Path).Connect()
|
new ObservableCollection<AbsolutePath>()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,9 +205,9 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
|
|||||||
pointInTime,
|
pointInTime,
|
||||||
parentFullName,
|
parentFullName,
|
||||||
AbsolutePathType.Container);
|
AbsolutePathType.Container);
|
||||||
var exceptions = new SourceList<Exception>();
|
var exceptions = new ObservableCollection<Exception>();
|
||||||
|
|
||||||
var children = new SourceCache<AbsolutePath, string>(i => i.Path.Path);
|
var children = new ObservableCollection<AbsolutePath>();
|
||||||
|
|
||||||
var container = new Container(
|
var container = new Container(
|
||||||
directoryInfo.Name,
|
directoryInfo.Name,
|
||||||
@@ -221,27 +224,30 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
|
|||||||
this,
|
this,
|
||||||
true,
|
true,
|
||||||
pointInTime,
|
pointInTime,
|
||||||
exceptions.Connect(),
|
exceptions,
|
||||||
new ExtensionCollection().AsReadOnly(),
|
new ExtensionCollection().AsReadOnly(),
|
||||||
children.Connect().StartWithEmpty()
|
children
|
||||||
);
|
);
|
||||||
|
|
||||||
Task.Run(() => LoadChildren(container, directoryInfo, children, pointInTime, exceptions));
|
if (initializeChildren)
|
||||||
|
{
|
||||||
|
Task.Run(async () => await LoadChildren(container, directoryInfo, children, pointInTime, exceptions));
|
||||||
|
}
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadChildren(Container container,
|
private async Task LoadChildren(Container container,
|
||||||
DirectoryInfo directoryInfo,
|
DirectoryInfo directoryInfo,
|
||||||
SourceCache<AbsolutePath, string> children,
|
ObservableCollection<AbsolutePath> children,
|
||||||
PointInTime pointInTime,
|
PointInTime pointInTime,
|
||||||
SourceList<Exception> exceptions)
|
ObservableCollection<Exception> exceptions)
|
||||||
{
|
{
|
||||||
var lockObj = new object();
|
var lockObj = new object();
|
||||||
var loadingIndicatorCancellation = new CancellationTokenSource();
|
var loadingIndicatorCancellation = new CancellationTokenSource();
|
||||||
|
|
||||||
Task.Run(DelayedLoadingIndicator);
|
Task.Run(async () => await DelayedLoadingIndicator());
|
||||||
LoadChildren();
|
await LoadChildrenInternal();
|
||||||
|
|
||||||
lock (lockObj)
|
lock (lockObj)
|
||||||
{
|
{
|
||||||
@@ -249,23 +255,17 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
|
|||||||
container.StopLoading();
|
container.StopLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadChildren()
|
Task LoadChildrenInternal()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var directory in directoryInfo.EnumerateDirectories())
|
foreach (var directory in directoryInfo.EnumerateDirectories())
|
||||||
{
|
{
|
||||||
if (container.LoadingCancellationToken.IsCancellationRequested) break;
|
try
|
||||||
var absolutePath = DirectoryToAbsolutePath(directory, pointInTime);
|
|
||||||
children.AddOrUpdate(absolutePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var file in directoryInfo.EnumerateFiles())
|
|
||||||
{
|
{
|
||||||
if (container.LoadingCancellationToken.IsCancellationRequested) break;
|
if (container.LoadingCancellationToken.IsCancellationRequested) break;
|
||||||
var absolutePath = FileToAbsolutePath(file, pointInTime);
|
var absolutePath = DirectoryToAbsolutePath(directory, pointInTime);
|
||||||
children.AddOrUpdate(absolutePath);
|
children.Add(absolutePath);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -273,12 +273,34 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var file in directoryInfo.EnumerateFiles())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (container.LoadingCancellationToken.IsCancellationRequested) break;
|
||||||
|
var absolutePath = FileToAbsolutePath(file, pointInTime);
|
||||||
|
children.Add(absolutePath);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
exceptions.Add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
exceptions.Add(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
async Task DelayedLoadingIndicator()
|
async Task DelayedLoadingIndicator()
|
||||||
{
|
{
|
||||||
var token = loadingIndicatorCancellation.Token;
|
var token = loadingIndicatorCancellation.Token;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(500, token);
|
await Task.Delay(2000, token);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -330,7 +352,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
|
|||||||
GetFileAttributes(fileInfo),
|
GetFileAttributes(fileInfo),
|
||||||
this,
|
this,
|
||||||
pointInTime,
|
pointInTime,
|
||||||
new SourceList<Exception>().Connect(),
|
new ObservableCollection<Exception>(),
|
||||||
extensions.AsReadOnly()
|
extensions.AsReadOnly()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user