From 697339768f15ca18e9aeafab52f1ed96f7b96fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Wed, 23 Feb 2022 13:58:12 +0100 Subject: [PATCH] Recursive container size scan --- .../FileTime.App.Core/Command/Commands.cs | 1 + .../DependencyInjection.cs | 3 + src/Core/AsyncEvent/AsyncEventHandler.cs | 2 +- .../ContainerScanSnapshotProvider.cs | 33 ++++ .../ContainerSizeContainer.cs | 85 +++++++++ .../ContainerSizeElement.cs | 39 ++++ .../ContainerSizeScanner/ScanSizeTask.cs | 128 +++++++++++++ .../FileTime.Core/Models/IItemWithSize.cs | 7 + .../Providers/AbstractContainer.cs | 1 + .../ContainerProperty/IHaveAttributes.cs | 7 + .../ContainerProperty/IHaveCreatedAt.cs | 7 + .../Providers/ContentProviderBase.cs | 35 +++- .../Providers/LazyLoadingContainer.cs | 74 ++++++++ .../FileTime.Core/Search/SearchContainer.cs | 4 +- src/GuiApp/FileTime.Avalonia/App.axaml | 3 + .../Application/TabContainer.cs | 12 ++ .../Configuration/MainConfiguration.cs | 1 + .../Converters/FormatSizeConverter.cs | 1 + .../Converters/ItemSizeToBrushConverter.cs | 41 ++++ .../Converters/ItemSizeToSizeConverter.cs | 26 +++ .../ItemViewModelIsAttibuteTypeConverter.cs | 4 +- .../FileTime.Avalonia.csproj | 1 + .../FileTime.Avalonia/Misc/ColorHelpers.cs | 178 ++++++++++++++++++ .../FileTime.Avalonia/Models/AttibuteType.cs | 3 +- .../Models/ItemPreviewMode.cs | 3 +- .../Services/CommandHandlerService.cs | 18 +- src/GuiApp/FileTime.Avalonia/Startup.cs | 3 + .../ViewModels/ContainerViewModel.cs | 17 ++ .../ItemPreview/ElementPreviewViewModel.cs | 2 + .../ItemPreview/IItemPreviewViewModel.cs | 2 + .../ItemPreview/ISizeItemViewModel.cs | 11 ++ .../ItemPreview/SearchContainerPreview.cs | 1 + .../ItemPreview/SearchElementPreview.cs | 1 + .../ItemPreview/SizeContainerPreview.cs | 109 +++++++++++ .../ItemPreview/SizeContainerViewModel.cs | 110 +++++++++++ .../ItemPreview/SizeElementViewmodel.cs | 24 +++ .../FileTime.Avalonia/Views/ItemView.axaml | 9 + .../FileTime.Avalonia/Views/MainWindow.axaml | 150 +++++++++++---- .../LocalContentProvider.cs | 2 +- .../FileTime.Providers.Local/LocalFolder.cs | 6 +- .../SftpContentProvider.cs | 2 +- .../FileTime.Providers.Sftp/SftpServer.cs | 1 + .../SmbContentProvider.cs | 2 +- .../FileTime.Providers.Smb/SmbServer.cs | 1 + 44 files changed, 1112 insertions(+), 58 deletions(-) create mode 100644 src/Core/FileTime.Core/ContainerSizeScanner/ContainerScanSnapshotProvider.cs create mode 100644 src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeContainer.cs create mode 100644 src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeElement.cs create mode 100644 src/Core/FileTime.Core/ContainerSizeScanner/ScanSizeTask.cs create mode 100644 src/Core/FileTime.Core/Models/IItemWithSize.cs create mode 100644 src/Core/FileTime.Core/Providers/ContainerProperty/IHaveAttributes.cs create mode 100644 src/Core/FileTime.Core/Providers/ContainerProperty/IHaveCreatedAt.cs create mode 100644 src/Core/FileTime.Core/Providers/LazyLoadingContainer.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Converters/ItemSizeToBrushConverter.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Converters/ItemSizeToSizeConverter.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Misc/ColorHelpers.cs create mode 100644 src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/ISizeItemViewModel.cs create mode 100644 src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeContainerPreview.cs create mode 100644 src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeContainerViewModel.cs create mode 100644 src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeElementViewmodel.cs diff --git a/src/AppCommon/FileTime.App.Core/Command/Commands.cs b/src/AppCommon/FileTime.App.Core/Command/Commands.cs index 6fada28..9de49cc 100644 --- a/src/AppCommon/FileTime.App.Core/Command/Commands.cs +++ b/src/AppCommon/FileTime.App.Core/Command/Commands.cs @@ -44,6 +44,7 @@ namespace FileTime.App.Core.Command Refresh, Rename, RunCommand, + ScanContainerSize, ShowAllShotcut, SoftDelete, SwitchToLastTab, diff --git a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs index 28e5bc2..ad17d44 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs +++ b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs @@ -1,6 +1,7 @@ using FileTime.App.Core.Clipboard; using FileTime.Core.Command; using FileTime.Core.CommandHandlers; +using FileTime.Core.ContainerSizeScanner; using FileTime.Core.Providers; using FileTime.Core.Services; using FileTime.Core.Timeline; @@ -22,6 +23,8 @@ namespace FileTime.App.Core .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton(p => p.GetService() ?? throw new ArgumentException(nameof(ContainerScanSnapshotProvider) + " is not registered")) .AddSingleton() .AddLocalServices() .AddSmbServices() diff --git a/src/Core/AsyncEvent/AsyncEventHandler.cs b/src/Core/AsyncEvent/AsyncEventHandler.cs index 8b9a6e2..4408cc3 100644 --- a/src/Core/AsyncEvent/AsyncEventHandler.cs +++ b/src/Core/AsyncEvent/AsyncEventHandler.cs @@ -48,7 +48,7 @@ List>? handlers; lock (_guard) { - handlers = _handlers; + handlers = new List>(_handlers); } foreach (var handler in handlers) diff --git a/src/Core/FileTime.Core/ContainerSizeScanner/ContainerScanSnapshotProvider.cs b/src/Core/FileTime.Core/ContainerSizeScanner/ContainerScanSnapshotProvider.cs new file mode 100644 index 0000000..6e81046 --- /dev/null +++ b/src/Core/FileTime.Core/ContainerSizeScanner/ContainerScanSnapshotProvider.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using FileTime.Core.Models; +using FileTime.Core.Providers; + +namespace FileTime.Core.ContainerSizeScanner +{ + public class ContainerScanSnapshotProvider : ContentProviderBase + { + + public ContainerScanSnapshotProvider() : base("size", null, "size://", false) + { + } + + public override Task CanHandlePath(string path) => Task.FromResult(path.StartsWith(Protocol)); + + public override Task CreateContainerAsync(string name) => throw new NotSupportedException(); + + public override Task CreateElementAsync(string name) => throw new NotSupportedException(); + + public async Task AddSnapshotAsync(ContainerSizeContainer snapshot) + { + if (RootContainers != null) + { + RootContainers.Add(snapshot); + while (RootContainers.Count > 10) + { + RootContainers.RemoveAt(0); + } + await RefreshAsync(); + } + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeContainer.cs b/src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeContainer.cs new file mode 100644 index 0000000..4db9272 --- /dev/null +++ b/src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeContainer.cs @@ -0,0 +1,85 @@ +using AsyncEvent; +using FileTime.Core.Models; +using FileTime.Core.Providers; +using FileTime.Core.Providers.ContainerProperty; + +namespace FileTime.Core.ContainerSizeScanner +{ + public class ContainerSizeContainer : LazyLoadingContainer, IItemWithSize, IHaveCreatedAt, IHaveAttributes + { + public override bool IsExists => true; + + public long? Size { get; private set; } + public AsyncEventHandler SizeChanged { get; } = new(); + + public bool AllowSizeScan { get; private set; } = true; + + private readonly IContainer _baseContainer; + + public string Attributes => _baseContainer is IHaveAttributes haveAttributes ? haveAttributes.Attributes : ""; + + public DateTime? CreatedAt => _baseContainer is IHaveCreatedAt haveCreatedAt ? haveCreatedAt.CreatedAt : null; + + public ContainerSizeContainer(ContainerScanSnapshotProvider provider, IContainer parent, IContainer baseContainer, string? displayName = null) : base(provider, parent, baseContainer.Name) + { + _baseContainer = baseContainer; + AllowRecursiveDeletion = false; + CanHandleEscape = true; + if (displayName != null) + { + DisplayName = displayName; + } + } + + public override Task CloneAsync() => Task.FromResult((IContainer)this); + + public override Task CreateContainerAsync(string name) => throw new NotSupportedException(); + + public override Task CreateElementAsync(string name) => throw new NotSupportedException(); + + public override Task Delete(bool hardDelete = false) => throw new NotSupportedException(); + + public override async Task RefreshAsync(CancellationToken token = default) + { + if (Refreshed != null) await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty, token); + } + + public override Task> RefreshItems(CancellationToken token = default) => throw new NotImplementedException(); + + public override Task Rename(string newName) => throw new NotSupportedException(); + + public override async Task AddContainerAsync(ContainerSizeContainer container) + { + await base.AddContainerAsync(container); + container.SizeChanged.Add(ChildContainerSizeChanged); + } + + public override async Task AddElementAsync(ContainerSizeElement element) + { + await base.AddElementAsync(element); + } + + public IEnumerable GetItemsWithSize() => Containers.Cast().Concat(Elements); + + private async Task ChildContainerSizeChanged(object? sender, long? size, CancellationToken token) => await UpdateSize(); + + public async Task UpdateSize() + { + Size = Containers.Aggregate(0L, (sum, c) => sum + c.Size ?? 0) + + Elements.Aggregate(0L, (sum, e) => sum + e.Size); + + await SizeChanged.InvokeAsync(this, Size); + } + + public override Task HandleEscape() + { + if (AllowSizeScan) + { + AllowSizeScan = false; + return Task.FromResult(new ContainerEscapeResult(true)); + } + + return Task.FromResult(new ContainerEscapeResult(_baseContainer)); + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeElement.cs b/src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeElement.cs new file mode 100644 index 0000000..af04fad --- /dev/null +++ b/src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeElement.cs @@ -0,0 +1,39 @@ +using FileTime.Core.Models; +using FileTime.Core.Providers; + +namespace FileTime.Core.ContainerSizeScanner +{ + public class ContainerSizeElement : AbstractElement, IItemWithSize, IFile + { + private readonly IElement _element; + + public long Size { get; } + + long? IItemWithSize.Size => Size; + + public string Attributes => _element is IFile file ? file.Attributes : _element.GetType().Name.Split('.').Last(); + + public DateTime CreatedAt => _element is IFile file ? file.CreatedAt : DateTime.MinValue; + + public ContainerSizeElement(ContainerScanSnapshotProvider provider, IContainer parent, IElement element, long size) : base(provider, parent, element.Name) + { + Size = size; + CanDelete = SupportsDelete.False; + _element = element; + } + + public override Task Delete(bool hardDelete = false) => throw new NotSupportedException(); + + public override Task GetContent(CancellationToken token = default) => Task.FromResult("NotImplementedException"); + + public override Task GetContentReaderAsync() => throw new NotSupportedException(); + + public override Task GetContentWriterAsync() => throw new NotSupportedException(); + + public override Task GetElementSize(CancellationToken token = default) => Task.FromResult((long?)Size); + + public override string GetPrimaryAttributeText() => ""; + + public override Task Rename(string newName) => throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/ContainerSizeScanner/ScanSizeTask.cs b/src/Core/FileTime.Core/ContainerSizeScanner/ScanSizeTask.cs new file mode 100644 index 0000000..b2e604a --- /dev/null +++ b/src/Core/FileTime.Core/ContainerSizeScanner/ScanSizeTask.cs @@ -0,0 +1,128 @@ +using FileTime.Core.Models; + +namespace FileTime.Core.ContainerSizeScanner +{ + public class ScanSizeTask + { + private readonly object _scanGuard = new(); + private bool _scannig; + private CancellationTokenSource? _cancellationTokenSource; + + public IContainer ContainerToScan { get; } + public ContainerSizeContainer Snapshot { get; } + + public bool Scanning + { + get + { + lock (_scanGuard) + { + return _scannig; + } + } + } + + public ScanSizeTask(ContainerScanSnapshotProvider provider, IContainer containerToScan) + { + ContainerToScan = containerToScan; + Snapshot = new ContainerSizeContainer(provider, provider, containerToScan, "Size scan on " + containerToScan.DisplayName); + Task.Run(async () => await provider.AddSnapshotAsync(Snapshot)).Wait(); + } + + public void Start() + { + lock (_scanGuard) + { + if (_scannig) return; + _scannig = true; + } + + new Thread(BootstrapScan).Start(); + + void BootstrapScan() + { + try + { + Task.Run(async () => await Snapshot.RunWithLazyLoading(async (token) => await ScanAsync(ContainerToScan, Snapshot, token))).Wait(); + } + finally + { + lock (_scanGuard) + { + _scannig = false; + _cancellationTokenSource = null; + } + } + } + } + + private async Task ScanAsync(IContainer container, ContainerSizeContainer targetContainer, CancellationToken token) + { + if (IsScanCancelled(targetContainer)) return; + + var childElements = await container.GetElements(token); + if (childElements != null) + { + foreach (var childElement in childElements) + { + if (token.IsCancellationRequested + || IsScanCancelled(targetContainer)) + { + return; + } + var newSizeElement = new ContainerSizeElement(targetContainer.Provider, targetContainer, childElement, await childElement.GetElementSize(token) ?? 0); + await targetContainer.AddElementAsync(newSizeElement); + } + } + + var childContainers = await container.GetContainers(token); + if (childContainers != null) + { + var newSizeContainers = new List<(ContainerSizeContainer, IContainer)>(); + foreach (var childContainer in childContainers) + { + var newSizeContainer = new ContainerSizeContainer(targetContainer.Provider, targetContainer, childContainer); + await targetContainer.AddContainerAsync(newSizeContainer); + newSizeContainers.Add((newSizeContainer, childContainer)); + } + + foreach (var (newSizeContainer, childContainer) in newSizeContainers) + { + if (token.IsCancellationRequested + || IsScanCancelled(newSizeContainer)) + { + return; + } + await newSizeContainer.RunWithLazyLoading(async (token) => await ScanAsync(childContainer, newSizeContainer, token), token); + } + } + + await targetContainer.UpdateSize(); + } + + private static bool IsScanCancelled(ContainerSizeContainer container) + { + IContainer? parent = container; + while (parent is ContainerSizeContainer sizeContainer) + { + if (!sizeContainer.AllowSizeScan) + { + return true; + } + parent = parent.GetParent(); + } + + return false; + } + + public void Cancel() + { + lock (_scanGuard) + { + if (_scannig || _cancellationTokenSource == null) return; + _cancellationTokenSource.Cancel(); + _cancellationTokenSource = null; + } + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Models/IItemWithSize.cs b/src/Core/FileTime.Core/Models/IItemWithSize.cs new file mode 100644 index 0000000..92da4a5 --- /dev/null +++ b/src/Core/FileTime.Core/Models/IItemWithSize.cs @@ -0,0 +1,7 @@ +namespace FileTime.Core.Models +{ + public interface IItemWithSize : IItem + { + long? Size { get; } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Providers/AbstractContainer.cs b/src/Core/FileTime.Core/Providers/AbstractContainer.cs index 6411fc8..1382300 100644 --- a/src/Core/FileTime.Core/Providers/AbstractContainer.cs +++ b/src/Core/FileTime.Core/Providers/AbstractContainer.cs @@ -68,6 +68,7 @@ namespace FileTime.Core.Providers DisplayName = Name = name; Exceptions = _exceptions.AsReadOnly(); Provider = null!; + AllowRecursiveDeletion = true; } public virtual Task CanOpenAsync() => Task.FromResult(_exceptions.Count == 0); diff --git a/src/Core/FileTime.Core/Providers/ContainerProperty/IHaveAttributes.cs b/src/Core/FileTime.Core/Providers/ContainerProperty/IHaveAttributes.cs new file mode 100644 index 0000000..e9d9633 --- /dev/null +++ b/src/Core/FileTime.Core/Providers/ContainerProperty/IHaveAttributes.cs @@ -0,0 +1,7 @@ +namespace FileTime.Core.Providers.ContainerProperty +{ + public interface IHaveAttributes + { + string Attributes { get; } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Providers/ContainerProperty/IHaveCreatedAt.cs b/src/Core/FileTime.Core/Providers/ContainerProperty/IHaveCreatedAt.cs new file mode 100644 index 0000000..11d6361 --- /dev/null +++ b/src/Core/FileTime.Core/Providers/ContainerProperty/IHaveCreatedAt.cs @@ -0,0 +1,7 @@ +namespace FileTime.Core.Providers.ContainerProperty +{ + public interface IHaveCreatedAt + { + DateTime? CreatedAt { get; } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Providers/ContentProviderBase.cs b/src/Core/FileTime.Core/Providers/ContentProviderBase.cs index 9225454..f4ec515 100644 --- a/src/Core/FileTime.Core/Providers/ContentProviderBase.cs +++ b/src/Core/FileTime.Core/Providers/ContentProviderBase.cs @@ -7,10 +7,11 @@ namespace FileTime.Core.Providers where T : class, IContentProvider { private readonly object _initializationGuard = new(); + protected IReadOnlyList _rootContainers; private bool _initialized; private bool _initializing; private IContainer? _parent; - protected IReadOnlyList? RootContainers { get; private set; } + protected List? RootContainers { get; private set; } public override bool IsExists => true; protected ContentProviderBase( @@ -22,6 +23,9 @@ namespace FileTime.Core.Providers { Protocol = protocol; SupportsContentStreams = supportsContentStreams; + RootContainers = new List(); + _rootContainers = RootContainers.AsReadOnly(); + AllowRecursiveDeletion = false; CanRename = false; CanDelete = SupportsDelete.False; @@ -39,21 +43,35 @@ namespace FileTime.Core.Providers protected async Task AddRootContainer(IContainer newRootContainer) { RootContainers = - (await GetContainers())?.Append(newRootContainer).OrderBy(c => c.Name).ToList().AsReadOnly() - ?? new List() { newRootContainer }.AsReadOnly(); + (await GetContainers())?.Append(newRootContainer).OrderBy(c => c.Name).ToList() + ?? new List() { newRootContainer }; + + _rootContainers = RootContainers.AsReadOnly(); + await RefreshAsync(); } protected async Task AddRootContainers(IEnumerable newRootContainers) { RootContainers = - (await GetContainers())?.Concat(newRootContainers).OrderBy(c => c.Name).ToList().AsReadOnly() - ?? new List(newRootContainers).AsReadOnly(); + (await GetContainers())?.Concat(newRootContainers).OrderBy(c => c.Name).ToList() + ?? new List(newRootContainers); + + _rootContainers = RootContainers.AsReadOnly(); + await RefreshAsync(); } - protected void SetRootContainers(IEnumerable newRootContainers) - => RootContainers = newRootContainers.OrderBy(c => c.Name).ToList().AsReadOnly(); + protected async Task SetRootContainers(IEnumerable newRootContainers) + { + RootContainers = newRootContainers.OrderBy(c => c.Name).ToList(); + _rootContainers = RootContainers.AsReadOnly(); + await RefreshAsync(); + } - protected void ClearRootContainers() => RootContainers = new List().AsReadOnly(); + protected void ClearRootContainers() + { + RootContainers = new List(); + _rootContainers = RootContainers.AsReadOnly(); + } public override async Task?> GetContainers(CancellationToken token = default) { @@ -120,7 +138,6 @@ namespace FileTime.Core.Providers public override Task Rename(string newName) => throw new NotSupportedException(); public override Task CanOpenAsync() => Task.FromResult(true); - public override void Unload() { } public override void Destroy() { } diff --git a/src/Core/FileTime.Core/Providers/LazyLoadingContainer.cs b/src/Core/FileTime.Core/Providers/LazyLoadingContainer.cs new file mode 100644 index 0000000..8a86614 --- /dev/null +++ b/src/Core/FileTime.Core/Providers/LazyLoadingContainer.cs @@ -0,0 +1,74 @@ +using AsyncEvent; +using FileTime.Core.Models; + +namespace FileTime.Core.Providers +{ + public abstract class LazyLoadingContainer : AbstractContainer + where TProvider : class, IContentProvider + where TContainer : class, IContainer + where TElement : class, IElement + { + protected List Containers { get; } + private IReadOnlyList _items; + protected List Elements { get; } + + private readonly IReadOnlyList _containersReadOnly; + private readonly IReadOnlyList _elementsReadOnly; + + protected LazyLoadingContainer(TProvider provider, IContainer parent, string name) : base(provider, parent, name) + { + Containers = new List(); + Elements = new List(); + + _containersReadOnly = Containers.AsReadOnly(); + _elementsReadOnly = Elements.AsReadOnly(); + _items = Containers.Cast().Concat(Elements).ToList().AsReadOnly(); + } + + public async Task RunWithLazyLoading(Func func, CancellationToken token = default) + { + try + { + LazyLoading = true; + await LazyLoadingChanged.InvokeAsync(this, LazyLoading, token); + await func(token); + } + finally + { + LazyLoading = false; + await LazyLoadingChanged.InvokeAsync(this, LazyLoading, token); + } + } + + public virtual async Task AddContainerAsync(TContainer container) + { + Containers.Add(container); + await UpdateChildren(); + } + + public virtual async Task AddElementAsync(TElement element) + { + Elements.Add(element); + await UpdateChildren(); + } + + private async Task UpdateChildren() + { + _items = Containers.Cast().Concat(Elements).ToList().AsReadOnly(); + await RefreshAsync(); + } + + public override async Task RefreshAsync(CancellationToken token = default) + { + if (Refreshed != null) await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty, token); + } + + public override Task> RefreshItems(CancellationToken token = default) => Task.FromResult(Enumerable.Empty()); + + public override Task?> GetContainers(CancellationToken token = default) => Task.FromResult((IReadOnlyList?)_containersReadOnly); + + public override Task?> GetElements(CancellationToken token = default) => Task.FromResult((IReadOnlyList?)_elementsReadOnly); + + public override Task?> GetItems(CancellationToken token = default) => Task.FromResult((IReadOnlyList?)_items); + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Search/SearchContainer.cs b/src/Core/FileTime.Core/Search/SearchContainer.cs index f16a426..4d73196 100644 --- a/src/Core/FileTime.Core/Search/SearchContainer.cs +++ b/src/Core/FileTime.Core/Search/SearchContainer.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using AsyncEvent; using FileTime.Core.Models; using FileTime.Core.Providers; @@ -23,9 +22,9 @@ namespace FileTime.Core.Search public SearchContainer(IContainer searchBaseContainer, SearchTaskBase searchTaskBase) : base(searchBaseContainer.Provider, searchBaseContainer.GetParent()!, searchBaseContainer.Name) { SearchBaseContainer = searchBaseContainer; + SearchTaskBase = searchTaskBase; _containers = new List(); _elements = new List(); - SearchTaskBase = searchTaskBase; _containersReadOnly = _containers.AsReadOnly(); _elementsReadOnly = _elements.AsReadOnly(); @@ -33,6 +32,7 @@ namespace FileTime.Core.Search UseLazyLoad = true; CanHandleEscape = true; + CanDelete = SupportsDelete.False; } public async Task RunWithLazyLoading(Func func, CancellationToken token = default) diff --git a/src/GuiApp/FileTime.Avalonia/App.axaml b/src/GuiApp/FileTime.Avalonia/App.axaml index 38c392d..69d8a67 100644 --- a/src/GuiApp/FileTime.Avalonia/App.axaml +++ b/src/GuiApp/FileTime.Avalonia/App.axaml @@ -148,6 +148,9 @@ + + + diff --git a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs index e856aa0..b322bea 100644 --- a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs +++ b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs @@ -15,6 +15,7 @@ using FileTime.Avalonia.ViewModels.ItemPreview; using FileTime.Core.Search; using Microsoft.Extensions.DependencyInjection; using FileTime.Core.Services; +using FileTime.Core.ContainerSizeScanner; namespace FileTime.Avalonia.Application { @@ -214,6 +215,12 @@ namespace FileTime.Avalonia.Application if (currentSelectenItem == null) { + } + else if (currentSelectenItem.Item is ContainerSizeContainer sizeContainer) + { + var sizeContainerPreview = _serviceProvider.GetService()!; + sizeContainerPreview.Init(sizeContainer); + preview = sizeContainerPreview; } else if (currentSelectenItem.Item is ChildSearchContainer searchContainer) { @@ -233,6 +240,11 @@ namespace FileTime.Avalonia.Application await elementPreview.Init(elementViewModel.Element); preview = elementPreview; } + + if (ItemPreview != null) + { + await ItemPreview.Destroy(); + } ItemPreview = preview; /*} catch diff --git a/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs b/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs index b9da353..aa71e19 100644 --- a/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs +++ b/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs @@ -78,6 +78,7 @@ namespace FileTime.Avalonia.Configuration new CommandBindingConfiguration(Commands.Rename, Key.F2), new CommandBindingConfiguration(Commands.Rename, new[] { Key.C, Key.W }), new CommandBindingConfiguration(Commands.RunCommand, new KeyConfig(Key.D4, shift: true)), + new CommandBindingConfiguration(Commands.ScanContainerSize, new[] { Key.C, Key.S }), new CommandBindingConfiguration(Commands.ShowAllShotcut, Key.F1), new CommandBindingConfiguration(Commands.SoftDelete, new[] { new KeyConfig(Key.D), new KeyConfig(Key.D, shift: true) }), new CommandBindingConfiguration(Commands.SwitchToLastTab, Key.D9), diff --git a/src/GuiApp/FileTime.Avalonia/Converters/FormatSizeConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/FormatSizeConverter.cs index a52dcbf..0978fe0 100644 --- a/src/GuiApp/FileTime.Avalonia/Converters/FormatSizeConverter.cs +++ b/src/GuiApp/FileTime.Avalonia/Converters/FormatSizeConverter.cs @@ -17,6 +17,7 @@ namespace FileTime.Avalonia.Converters { (long size, true) => ToSizeString(size, prec), (long size, false) => ToSizeString(size), + (null, _) => "...", _ => value }; } diff --git a/src/GuiApp/FileTime.Avalonia/Converters/ItemSizeToBrushConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/ItemSizeToBrushConverter.cs new file mode 100644 index 0000000..60430c4 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Converters/ItemSizeToBrushConverter.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia.Threading; +using FileTime.Avalonia.Misc; +using FileTime.Avalonia.ViewModels.ItemPreview; + +namespace FileTime.Avalonia.Converters +{ + public class ItemSizeToBrushConverter : IMultiValueConverter + { + public double HueDiff { get; set; } + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values.Count == 2 + && values[0] is ISizeItemViewModel sizeContainerViewModel + && values[1] is IList items) + { + + int i = 0; + for (; i < items.Count; i++) + { + if (items[i].Item == sizeContainerViewModel.Item) break; + } + + var hue = (360d * i / (items.Count < 1 ? 1 : items.Count)) + HueDiff; + if (hue > 360) hue -= 360; + if (hue < 0) hue += 360; + + var (r, g, b) = ColorHelper.HlsToRgb(hue, 0.5, 1); + var task = Dispatcher.UIThread.InvokeAsync(() => new SolidColorBrush(Color.FromRgb(r, g, b))); + task.Wait(); + return task.Result; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Converters/ItemSizeToSizeConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/ItemSizeToSizeConverter.cs new file mode 100644 index 0000000..6de5967 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Converters/ItemSizeToSizeConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia.Data.Converters; +using FileTime.Avalonia.ViewModels.ItemPreview; + +namespace FileTime.Avalonia.Converters +{ + public class ItemSizeToSizeConverter : IMultiValueConverter + { + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values.Count == 3 + && values[0] is ISizeItemViewModel sizeContainerViewModel + && values[1] is IEnumerable items + && values[2] is double width && width > 0) + { + var commulativeSize = items.Select(i => i.Size).Sum(); + return width * sizeContainerViewModel.Size / commulativeSize; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Converters/ItemViewModelIsAttibuteTypeConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/ItemViewModelIsAttibuteTypeConverter.cs index df9a1cb..4735087 100644 --- a/src/GuiApp/FileTime.Avalonia/Converters/ItemViewModelIsAttibuteTypeConverter.cs +++ b/src/GuiApp/FileTime.Avalonia/Converters/ItemViewModelIsAttibuteTypeConverter.cs @@ -3,6 +3,7 @@ using System.Globalization; using Avalonia.Data.Converters; using FileTime.Avalonia.Models; using FileTime.Avalonia.ViewModels; +using FileTime.Core.ContainerSizeScanner; using FileTime.Core.Models; using FileTime.Providers.Local; @@ -30,8 +31,9 @@ namespace FileTime.Avalonia.Converters } return AttibuteType.Element; } - else if (value is ContainerViewModel) + else if (value is ContainerViewModel containerVM) { + if(containerVM.BaseItem is ContainerSizeContainer) return AttibuteType.SizeContainer; return AttibuteType.Container; } diff --git a/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj b/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj index 148ec9d..411f8d5 100644 --- a/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj +++ b/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj @@ -47,6 +47,7 @@ + diff --git a/src/GuiApp/FileTime.Avalonia/Misc/ColorHelpers.cs b/src/GuiApp/FileTime.Avalonia/Misc/ColorHelpers.cs new file mode 100644 index 0000000..8912282 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Misc/ColorHelpers.cs @@ -0,0 +1,178 @@ +namespace FileTime.Avalonia.Misc +{ + public static class ColorHelper + { + /*public struct RGB + { + public RGB(byte r, byte g, byte b) + { + R = r; + G = g; + B = b; + } + + public byte R { get; set; } + + public byte G { get; set; } + + public byte B { get; set; } + + public bool Equals(RGB rgb) + { + return (R == rgb.R) && (G == rgb.G) && (B == rgb.B); + } + } + + public struct HSL + { + public HSL(int h, float s, float l) + { + H = h; + S = s; + L = l; + } + + public int H { get; set; } + + public float S { get; set; } + + public float L { get; set; } + + public bool Equals(HSL hsl) + { + return (H == hsl.H) && (S == hsl.S) && (L == hsl.L); + } + } + + public static RGB HSLToRGB(HSL hsl) + { + byte r = 0; + byte g = 0; + byte b = 0; + + if (hsl.S == 0) + { + r = g = b = (byte)(hsl.L * 255); + } + else + { + float v1, v2; + float hue = (float)hsl.H / 360; + + v2 = (hsl.L < 0.5) ? (hsl.L * (1 + hsl.S)) : ((hsl.L + hsl.S) - (hsl.L * hsl.S)); + v1 = 2 * hsl.L - v2; + + r = (byte)(255 * HueToRGB(v1, v2, hue + (1.0f / 3))); + g = (byte)(255 * HueToRGB(v1, v2, hue)); + b = (byte)(255 * HueToRGB(v1, v2, hue - (1.0f / 3))); + } + + return new RGB(r, g, b); + } + + private static float HueToRGB(float v1, float v2, float vH) + { + if (vH < 0) + vH += 1; + + if (vH > 1) + vH -= 1; + + if ((6 * vH) < 1) + return (v1 + (v2 - v1) * 6 * vH); + + if ((2 * vH) < 1) + return v2; + + if ((3 * vH) < 2) + return (v1 + (v2 - v1) * ((2.0f / 3) - vH) * 6); + + return v1; + }*/ + + /*public static (byte r, byte g, byte b) ColorFromHSL(double h, double s, double l) + { + double r = 0, g = 0, b = 0; + if (l != 0) + { + if (s == 0) + { + r = g = b = l; + } + else + { + double temp2; + if (l < 0.5) + temp2 = l * (1.0 + s); + else + temp2 = l + s - (l * s); + + double temp1 = 2.0 * l - temp2; + + r = GetColorComponent(temp1, temp2, h + 1.0 / 3.0); + g = GetColorComponent(temp1, temp2, h); + b = GetColorComponent(temp1, temp2, h - 1.0 / 3.0); + } + } + return ((byte)(255 * r), (byte)(255 * g), (byte)(255 * b)); + + } + + private static double GetColorComponent(double temp1, double temp2, double temp3) + { + if (temp3 < 0.0) + temp3 += 1.0; + else if (temp3 > 1.0) + temp3 -= 1.0; + + if (temp3 < 1.0 / 6.0) + return temp1 + (temp2 - temp1) * 6.0 * temp3; + else if (temp3 < 0.5) + return temp2; + else if (temp3 < 2.0 / 3.0) + return temp1 + ((temp2 - temp1) * ((2.0 / 3.0) - temp3) * 6.0); + else + return temp1; + }*/ + + + // Convert an HLS value into an RGB value. + public static (byte r, byte g, byte b) HlsToRgb(double h, double l, double s) + { + double p2; + if (l <= 0.5) p2 = l * (1 + s); + else p2 = l + s - l * s; + + double p1 = 2 * l - p2; + double double_r, double_g, double_b; + if (s == 0) + { + double_r = l; + double_g = l; + double_b = l; + } + else + { + double_r = QqhToRgb(p1, p2, h + 120); + double_g = QqhToRgb(p1, p2, h); + double_b = QqhToRgb(p1, p2, h - 120); + } + + // Convert RGB to the 0 to 255 range. + return ((byte)(double_r * 255.0), + (byte)(double_g * 255.0), + (byte)(double_b * 255.0)); + } + + private static double QqhToRgb(double q1, double q2, double hue) + { + if (hue > 360) hue -= 360; + else if (hue < 0) hue += 360; + + if (hue < 60) return q1 + (q2 - q1) * hue / 60; + if (hue < 180) return q2; + if (hue < 240) return q1 + (q2 - q1) * (240 - hue) / 60; + return q1; + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Models/AttibuteType.cs b/src/GuiApp/FileTime.Avalonia/Models/AttibuteType.cs index be613c7..ecf8683 100644 --- a/src/GuiApp/FileTime.Avalonia/Models/AttibuteType.cs +++ b/src/GuiApp/FileTime.Avalonia/Models/AttibuteType.cs @@ -4,6 +4,7 @@ namespace FileTime.Avalonia.Models { File, Element, - Container + Container, + SizeContainer } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Models/ItemPreviewMode.cs b/src/GuiApp/FileTime.Avalonia/Models/ItemPreviewMode.cs index b7d9ba6..39b64ba 100644 --- a/src/GuiApp/FileTime.Avalonia/Models/ItemPreviewMode.cs +++ b/src/GuiApp/FileTime.Avalonia/Models/ItemPreviewMode.cs @@ -6,6 +6,7 @@ namespace FileTime.Avalonia.Models Text, Empty, SearchContainer, - SearchElement + SearchElement, + SizeContainer, } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs b/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs index d270503..cd6580a 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs @@ -19,6 +19,7 @@ using FileTime.Core.Command.Delete; using FileTime.Core.Command.Move; using FileTime.Core.Command.Rename; using FileTime.Core.Components; +using FileTime.Core.ContainerSizeScanner; using FileTime.Core.Interactions; using FileTime.Core.Models; using FileTime.Core.Providers; @@ -47,6 +48,7 @@ namespace FileTime.Avalonia.Services private readonly ProgramsService _programsService; private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; + private readonly ContainerScanSnapshotProvider _containerScanSnapshotProvider; public CommandHandlerService( AppState appState, @@ -59,7 +61,8 @@ namespace FileTime.Avalonia.Services IEnumerable contentProviders, ProgramsService programsService, ILogger logger, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + ContainerScanSnapshotProvider containerScanSnapshotProvider) { _appState = appState; _localContentProvider = localContentProvider; @@ -72,6 +75,7 @@ namespace FileTime.Avalonia.Services _programsService = programsService; _logger = logger; _serviceProvider = serviceProvider; + _containerScanSnapshotProvider = containerScanSnapshotProvider; _commandHandlers = new Dictionary> { @@ -115,6 +119,7 @@ namespace FileTime.Avalonia.Services {Commands.Refresh, RefreshCurrentLocation}, {Commands.Rename, Rename}, {Commands.RunCommand, RunCommandInContainer}, + {Commands.ScanContainerSize, ScanContainerSize}, {Commands.ShowAllShotcut, ShowAllShortcut}, {Commands.SoftDelete, SoftDelete}, {Commands.SwitchToLastTab, async() => await SwitchToTab(-1)}, @@ -995,5 +1000,16 @@ namespace FileTime.Avalonia.Services return Task.CompletedTask; } + + private async Task ScanContainerSize() + { + if (_appState.SelectedTab.CurrentLocation != null) + { + var scanTask = new ScanSizeTask(_containerScanSnapshotProvider, _appState.SelectedTab.CurrentLocation.Container); + scanTask.Start(); + + await OpenContainer(scanTask.Snapshot); + } + } } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Startup.cs b/src/GuiApp/FileTime.Avalonia/Startup.cs index 9483c16..cae64b4 100644 --- a/src/GuiApp/FileTime.Avalonia/Startup.cs +++ b/src/GuiApp/FileTime.Avalonia/Startup.cs @@ -26,6 +26,9 @@ namespace FileTime.Avalonia .AddTransient() .AddTransient() .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() .AddSingleton(); } internal static IServiceCollection AddServices(this IServiceCollection serviceCollection) diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs index da1bc25..74b1952 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs @@ -14,6 +14,7 @@ using FileTime.Avalonia.Application; using System.Threading; using FileTime.Core.Services; using FileTime.Core.Search; +using FileTime.Core.ContainerSizeScanner; namespace FileTime.Avalonia.ViewModels { @@ -48,6 +49,9 @@ namespace FileTime.Avalonia.ViewModels [Property] private List _exceptions; + //[Property] + //private long? _size; + public IItem Item => _container; public IItem BaseItem => _baseContainer; @@ -81,6 +85,8 @@ namespace FileTime.Avalonia.ViewModels public Task Elements => GetElements(); public Task Items => GetItems(); + public long? Size => BaseContainer is ContainerSizeContainer sizeContainer ? sizeContainer.Size : null; + public async Task> GetContainers(CancellationToken token = default) { if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token); @@ -133,10 +139,21 @@ namespace FileTime.Avalonia.ViewModels Container = container; BaseContainer = container is ChildSearchContainer childSearchContainer ? childSearchContainer.BaseContainer : container; + if (BaseContainer is ContainerSizeContainer sizeContainer) + { + sizeContainer.SizeChanged.Add(UpdateSize); + } Container.Refreshed.Add(Container_Refreshed); Container.LazyLoadingChanged.Add(Container_LazyLoadingChanged); } + private Task UpdateSize(object? sender, long? size, CancellationToken token) + { + OnPropertyChanged(nameof(Size)); + + return Task.CompletedTask; + } + public void InvalidateDisplayName() => OnPropertyChanged(nameof(DisplayName)); public async Task Init(bool initializeChildren = true, CancellationToken token = default) diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/ElementPreviewViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/ElementPreviewViewModel.cs index bad7b70..63cbd25 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/ElementPreviewViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/ElementPreviewViewModel.cs @@ -53,5 +53,7 @@ namespace FileTime.Avalonia.ViewModels.ItemPreview Mode = ItemPreviewMode.Unknown; } } + + public Task Destroy() => Task.CompletedTask; } } diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/IItemPreviewViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/IItemPreviewViewModel.cs index ab00127..d9d2a29 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/IItemPreviewViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/IItemPreviewViewModel.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using FileTime.Avalonia.Models; namespace FileTime.Avalonia.ViewModels.ItemPreview @@ -5,5 +6,6 @@ namespace FileTime.Avalonia.ViewModels.ItemPreview public interface IItemPreviewViewModel { ItemPreviewMode Mode { get; } + Task Destroy(); } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/ISizeItemViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/ISizeItemViewModel.cs new file mode 100644 index 0000000..76a89c4 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/ISizeItemViewModel.cs @@ -0,0 +1,11 @@ +using FileTime.Core.Models; + +namespace FileTime.Avalonia.ViewModels.ItemPreview +{ + public interface ISizeItemViewModel + { + string? Name { get; } + long? Size { get; } + IItem? Item { get; } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchContainerPreview.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchContainerPreview.cs index c090caf..bfc8996 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchContainerPreview.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchContainerPreview.cs @@ -29,5 +29,6 @@ namespace FileTime.Avalonia.ViewModels.ItemPreview RealtiveParentPath = new AbsolutePath(null!, container.FullName!.Substring(pathCommonPath.Length).Trim(Constants.SeparatorChar), AbsolutePathType.Unknown, null).GetParentPath(); Task.Run(async () => ItemNameParts = await Dispatcher.UIThread.InvokeAsync(() => container.SearchDisplayName.ConvertAll(p => new ItemNamePartViewModel(p.Text, p.IsSpecial ? TextDecorations.Underline : null)))); } + public Task Destroy() => Task.CompletedTask; } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchElementPreview.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchElementPreview.cs index 21f91b1..cba87a6 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchElementPreview.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchElementPreview.cs @@ -31,5 +31,6 @@ namespace FileTime.Avalonia.ViewModels.ItemPreview RealtiveParentPath = new AbsolutePath(null!, element.FullName!.Substring(pathCommonPath.Length).Trim(Constants.SeparatorChar), AbsolutePathType.Unknown, null).GetParentPath(); Task.Run(async () => ItemNameParts = await Dispatcher.UIThread.InvokeAsync(() => element.SearchDisplayName.ConvertAll(p => new ItemNamePartViewModel(p.Text, p.IsSpecial ? TextDecorations.Underline : null)))); } + public Task Destroy() => Task.CompletedTask; } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeContainerPreview.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeContainerPreview.cs new file mode 100644 index 0000000..6dbb0ac --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeContainerPreview.cs @@ -0,0 +1,109 @@ +using AsyncEvent; +using FileTime.Avalonia.Models; +using FileTime.Core.ContainerSizeScanner; +using FileTime.Core.Models; +using Microsoft.Extensions.DependencyInjection; +using MvvmGen; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading; +using System.Threading.Tasks; + +namespace FileTime.Avalonia.ViewModels.ItemPreview +{ + [ViewModel] + [Inject(typeof(IServiceProvider), PropertyName = "_serviceProvider")] + public partial class SizeContainerPreview : IItemPreviewViewModel + { + private readonly BehaviorSubject _update = new BehaviorSubject(null); + private IEnumerable? _allItems; + + [Property] + private IObservable> _items; + [Property] + private IObservable> _topItems; + + public ItemPreviewMode Mode => ItemPreviewMode.SizeContainer; + + public void Init(ContainerSizeContainer container) + { + container.Refreshed.Add(ContainerRefreshed); + container.SizeChanged.Add(ItemsChanged); + Items = _update.Throttle(TimeSpan.FromMilliseconds(500)).Select((_) => RefreshItems(this, container)); + TopItems = Items.Select(items => items.Take(10)); + _update.OnNext(null); + } + + private IEnumerable RefreshItems(SizeContainerPreview parent, ContainerSizeContainer container) + { + if (_allItems != null) + { + foreach (var item in _allItems) + { + if (item is ContainerSizeContainer sizeContainer) + { + sizeContainer.SizeChanged.Remove(ItemsChanged); + } + } + } + + var items = GetItems(parent, container).ToList(); + foreach (var item in items) + { + if (item is ContainerSizeContainer sizeContainer) + { + sizeContainer.SizeChanged.Add(ItemsChanged); + } + } + + _allItems = items; + + return items; + } + + private Task ItemsChanged(object? sender, long? size, CancellationToken token) + { + _update.OnNext(null); + return Task.CompletedTask; + } + + private Task ContainerRefreshed(object? sender, AsyncEventArgs e, CancellationToken token) + { + _update.OnNext(null); + return Task.CompletedTask; + } + + public IEnumerable GetItems(SizeContainerPreview parent, ContainerSizeContainer container) + { + var items = new List(); + var itemsWithSize = container.GetItemsWithSize().OrderByDescending(i => i.Size).ToList(); + + foreach (var itemWithSize in itemsWithSize) + { + if (itemWithSize is ContainerSizeContainer sizeContainer) + { + var containerVm = _serviceProvider.GetService()!; + containerVm.Init(parent, sizeContainer); + items.Add(containerVm); + } + else if (itemWithSize is ContainerSizeElement sizeElement) + { + var elementVm = _serviceProvider.GetService()!; + elementVm.Init(sizeElement); + items.Add(elementVm); + } + else + { + throw new ArgumentException(); + } + } + + return items; + } + public Task Destroy() => Task.CompletedTask; + } +} diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeContainerViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeContainerViewModel.cs new file mode 100644 index 0000000..674342b --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeContainerViewModel.cs @@ -0,0 +1,110 @@ +using AsyncEvent; +using FileTime.Core.ContainerSizeScanner; +using FileTime.Core.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading; +using System.Threading.Tasks; + +namespace FileTime.Avalonia.ViewModels.ItemPreview +{ + public class SizeContainerViewModel : ISizeItemViewModel + { + private bool _initialized; + private SizeContainerPreview? _parent; + private ContainerSizeContainer? _sizeContainer; + + private readonly BehaviorSubject _update = new BehaviorSubject(null); + private IEnumerable? _allItems; + + private IObservable>? _items; + private IObservable>? _topItems; + + public IObservable>? Items + { + get + { + if(!_initialized) + { + _update.OnNext(null); + _initialized = true; + } + return _items; + } + } + public IObservable>? TopItems + { + get + { + if (!_initialized) + { + _update.OnNext(null); + _initialized = true; + } + + return _topItems; + } + } + + public string? Name { get; private set; } + public long? Size { get; private set; } + + public IItem? Item => _sizeContainer; + + public void Init(SizeContainerPreview parent, ContainerSizeContainer sizeContainer) + { + _sizeContainer = sizeContainer; + _parent = parent; + + Name = sizeContainer.DisplayName; + Size = sizeContainer.Size; + + sizeContainer.Refreshed.Add(ContainerRefreshed); + sizeContainer.SizeChanged.Add(ItemsChanged); + _items = _update.Throttle(TimeSpan.FromMilliseconds(500)).Select((_) => RefreshItems(parent, sizeContainer)); + _topItems = _items.Select(items => items.Take(10)); + } + + private IEnumerable RefreshItems(SizeContainerPreview parent, ContainerSizeContainer container) + { + if (_allItems != null) + { + foreach (var item in _allItems) + { + if (item is ContainerSizeContainer sizeContainer) + { + sizeContainer.SizeChanged.Remove(ItemsChanged); + } + } + } + + var items = parent.GetItems(parent, container).ToList(); + foreach (var item in items) + { + if (item is ContainerSizeContainer sizeContainer) + { + sizeContainer.SizeChanged.Add(ItemsChanged); + } + } + + _allItems = items; + + return items; + } + + private Task ItemsChanged(object? sender, long? size, CancellationToken token) + { + _update.OnNext(null); + return Task.CompletedTask; + } + + private Task ContainerRefreshed(object? sender, AsyncEventArgs e, CancellationToken token) + { + _update.OnNext(null); + return Task.CompletedTask; + } + } +} diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeElementViewmodel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeElementViewmodel.cs new file mode 100644 index 0000000..a89545f --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SizeElementViewmodel.cs @@ -0,0 +1,24 @@ +using FileTime.Core.ContainerSizeScanner; +using FileTime.Core.Models; +using MvvmGen; + +namespace FileTime.Avalonia.ViewModels.ItemPreview +{ + [ViewModel] + public partial class SizeElementViewmodel : ISizeItemViewModel + { + private ContainerSizeElement? _sizeElement; + public string? Name { get; private set; } + public long? Size { get; private set; } + + public IItem? Item => _sizeElement; + + public void Init(ContainerSizeElement sizeElement) + { + _sizeElement = sizeElement; + + Name = sizeElement.DisplayName; + Size = sizeElement.Size; + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml b/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml index 482b1ff..4ef76ab 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml +++ b/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml @@ -71,6 +71,15 @@ + + + + + diff --git a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml index 7d3dd6b..166a2eb 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml +++ b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml @@ -286,26 +286,25 @@ Fill="{DynamicResource ContentSeparatorBrush}" /> - - - - - - - - + + + + + + + + + - - - + Empty - - + - + - - - - - - - + + + + + + + + @@ -398,6 +397,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs index dab16e7..ecfe36a 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -21,7 +21,7 @@ namespace FileTime.Providers.Local ? new DirectoryInfo("/").GetDirectories() : Environment.GetLogicalDrives().Select(d => new DirectoryInfo(d)); - SetRootContainers(rootDirectories.Select(d => new LocalFolder(d, this, this)).OrderBy(d => d.Name)); + SetRootContainers(rootDirectories.Select(d => new LocalFolder(d, this, this)).OrderBy(d => d.Name)).Wait(); } public async Task GetByPath(string path, bool acceptDeepestMatch = false) diff --git a/src/Providers/FileTime.Providers.Local/LocalFolder.cs b/src/Providers/FileTime.Providers.Local/LocalFolder.cs index 5bfcc59..6bd1695 100644 --- a/src/Providers/FileTime.Providers.Local/LocalFolder.cs +++ b/src/Providers/FileTime.Providers.Local/LocalFolder.cs @@ -1,17 +1,18 @@ using System.Runtime.InteropServices; using FileTime.Core.Models; using FileTime.Core.Providers; +using FileTime.Core.Providers.ContainerProperty; using FileTime.Providers.Local.Interop; namespace FileTime.Providers.Local { - public class LocalFolder : AbstractContainer, IContainer + public class LocalFolder : AbstractContainer, IContainer, IHaveCreatedAt, IHaveAttributes { public DirectoryInfo Directory { get; } public string Attributes => GetAttributes(); - public DateTime CreatedAt => Directory.CreationTime; + public DateTime? CreatedAt => Directory.CreationTime; public override bool IsExists => Directory.Exists; public LocalFolder(DirectoryInfo directory, LocalContentProvider contentProvider, IContainer parent) @@ -21,7 +22,6 @@ namespace FileTime.Providers.Local NativePath = Directory.FullName; IsHidden = (Directory.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; CanRename = true; - AllowRecursiveDeletion = true; //TODO: Linux soft delete SupportsDirectoryLevelSoftDelete = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); diff --git a/src/Providers/FileTime.Providers.Sftp/SftpContentProvider.cs b/src/Providers/FileTime.Providers.Sftp/SftpContentProvider.cs index 05401fc..90915ac 100644 --- a/src/Providers/FileTime.Providers.Sftp/SftpContentProvider.cs +++ b/src/Providers/FileTime.Providers.Sftp/SftpContentProvider.cs @@ -17,7 +17,7 @@ namespace FileTime.Providers.Sftp _inputInterface = inputInterface; } - public override Task CanHandlePath(string path) => Task.FromResult(path.StartsWith("sftp://")); + public override Task CanHandlePath(string path) => Task.FromResult(path.StartsWith(Protocol)); public override async Task CreateContainerAsync(string name) { diff --git a/src/Providers/FileTime.Providers.Sftp/SftpServer.cs b/src/Providers/FileTime.Providers.Sftp/SftpServer.cs index 916f82b..7f61a7d 100644 --- a/src/Providers/FileTime.Providers.Sftp/SftpServer.cs +++ b/src/Providers/FileTime.Providers.Sftp/SftpServer.cs @@ -32,6 +32,7 @@ namespace FileTime.Providers.Sftp NativePath = FullName = sftpContentProvider.Protocol + Constants.SeparatorChar + name; CanDelete = SupportsDelete.True; + AllowRecursiveDeletion = false; } public override async Task> RefreshItems(CancellationToken token = default) => await ListDirectory(this, ""); diff --git a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs index 0708b1d..e371abf 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs @@ -91,7 +91,7 @@ namespace FileTime.Providers.Smb protected override async Task Init() { var servers = await _persistenceService.LoadServers(); - SetRootContainers(servers.Select(s => new SmbServer(s.Path, this, _inputInterface, s.UserName, s.Password))); + await SetRootContainers(servers.Select(s => new SmbServer(s.Path, this, _inputInterface, s.UserName, s.Password))); } public static string GetNativePathSeparator() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "\\" : "/"; diff --git a/src/Providers/FileTime.Providers.Smb/SmbServer.cs b/src/Providers/FileTime.Providers.Smb/SmbServer.cs index 3eae02f..ad391da 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbServer.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbServer.cs @@ -31,6 +31,7 @@ namespace FileTime.Providers.Smb Username = username; Password = password; CanDelete = SupportsDelete.True; + AllowRecursiveDeletion = false; FullName = contentProvider.Protocol + Name; NativePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)