From 2ccd887d870615c0e52011721ff703f4d522a34c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Tue, 22 Feb 2022 13:20:21 +0100 Subject: [PATCH] Search by name/regex --- .../FileTime.App.Core/Command/Commands.cs | 2 + .../DependencyInjection.cs | 2 + .../Command/Copy/CalculateOperation.cs | 2 +- src/Core/FileTime.Core/Components/Tab.cs | 55 +++----- src/Core/FileTime.Core/FileTime.Core.csproj | 1 + src/Core/FileTime.Core/Helper/PathHelper.cs | 55 ++++++++ .../Models/ContainerEscapeResult.cs | 18 +++ src/Core/FileTime.Core/Models/IContainer.cs | 6 + src/Core/FileTime.Core/Models/IElement.cs | 2 +- src/Core/FileTime.Core/Models/IItem.cs | 1 + .../FileTime.Core}/Models/ItemNamePart.cs | 2 +- .../FileTime.Core/Models/VirtualContainer.cs | 9 ++ .../Providers/AbstractContainer.cs | 13 +- .../Providers/AbstractElement.cs | 58 ++++++++ .../FileTime.Core/Providers/TopContainer.cs | 8 ++ .../Search/ChildSearchContainer.cs | 40 ++++++ .../Search/ChildSearchElement.cs | 33 +++++ .../Search/NameRegexSearchTask.cs | 17 +++ .../FileTime.Core/Search/NameSearchTask.cs | 21 +++ .../FileTime.Core/Search/SearchContainer.cs | 103 ++++++++++++++ .../FileTime.Core/Search/SearchTaskBase.cs | 127 ++++++++++++++++++ .../Services/ItemNameConverterService.cs | 29 ++-- .../FileTime.Core/Timeline/TimeContainer.cs | 10 +- .../FileTime.Core/Timeline/TimeElement.cs | 5 +- src/GuiApp/FileTime.Avalonia/App.axaml | 13 ++ .../Application/TabContainer.cs | 48 ++++--- .../FileTime.Avalonia/Assets/loading.svg | 3 + .../Configuration/MainConfiguration.cs | 2 + .../Converters/GetFileExtensionConverter.cs | 2 +- .../Converters/NamePartShrinkerConverter.cs | 2 +- .../Models/ElementPreviewMode.cs | 9 -- .../Models/ItemPreviewMode.cs | 11 ++ .../Services/CommandHandlerService.cs | 59 +++++++- .../Services/KeyInputHandlerService.cs | 40 +++++- .../Services/StatePersistenceService.cs | 10 +- src/GuiApp/FileTime.Avalonia/Startup.cs | 4 +- .../ViewModels/ContainerViewModel.cs | 20 ++- .../ViewModels/ElementViewModel.cs | 15 ++- .../ElementPreviewViewModel.cs | 26 ++-- .../ItemPreview/IItemPreviewViewModel.cs | 9 ++ .../ItemPreview/SearchContainerPreview.cs | 33 +++++ .../ItemPreview/SearchElementPreview.cs | 35 +++++ .../ViewModels/MainPageViewModel.cs | 5 +- .../FileTime.Avalonia/Views/MainWindow.axaml | 96 ++++++++++--- .../FileTime.Providers.Local/LocalFile.cs | 5 +- .../FileTime.Providers.Sftp/SftpFile.cs | 3 +- .../FileTime.Providers.Smb/SmbFile.cs | 5 +- 47 files changed, 929 insertions(+), 145 deletions(-) create mode 100644 src/Core/FileTime.Core/Helper/PathHelper.cs create mode 100644 src/Core/FileTime.Core/Models/ContainerEscapeResult.cs rename src/{GuiApp/FileTime.Avalonia => Core/FileTime.Core}/Models/ItemNamePart.cs (87%) create mode 100644 src/Core/FileTime.Core/Providers/AbstractElement.cs create mode 100644 src/Core/FileTime.Core/Search/ChildSearchContainer.cs create mode 100644 src/Core/FileTime.Core/Search/ChildSearchElement.cs create mode 100644 src/Core/FileTime.Core/Search/NameRegexSearchTask.cs create mode 100644 src/Core/FileTime.Core/Search/NameSearchTask.cs create mode 100644 src/Core/FileTime.Core/Search/SearchContainer.cs create mode 100644 src/Core/FileTime.Core/Search/SearchTaskBase.cs rename src/{GuiApp/FileTime.Avalonia => Core/FileTime.Core}/Services/ItemNameConverterService.cs (60%) create mode 100644 src/GuiApp/FileTime.Avalonia/Assets/loading.svg delete mode 100644 src/GuiApp/FileTime.Avalonia/Models/ElementPreviewMode.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Models/ItemPreviewMode.cs rename src/GuiApp/FileTime.Avalonia/ViewModels/{ => ItemPreview}/ElementPreviewViewModel.cs (64%) create mode 100644 src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/IItemPreviewViewModel.cs create mode 100644 src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchContainerPreview.cs create mode 100644 src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchElementPreview.cs diff --git a/src/AppCommon/FileTime.App.Core/Command/Commands.cs b/src/AppCommon/FileTime.App.Core/Command/Commands.cs index 1a74b40..6fada28 100644 --- a/src/AppCommon/FileTime.App.Core/Command/Commands.cs +++ b/src/AppCommon/FileTime.App.Core/Command/Commands.cs @@ -16,6 +16,8 @@ namespace FileTime.App.Core.Command Cut, Edit, EnterRapidTravel, + FindByName, + FindByNameRegex, GoToHome, GoToPath, GoToProvider, diff --git a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs index a547a10..28e5bc2 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs +++ b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs @@ -2,6 +2,7 @@ using FileTime.App.Core.Clipboard; using FileTime.Core.Command; using FileTime.Core.CommandHandlers; using FileTime.Core.Providers; +using FileTime.Core.Services; using FileTime.Core.Timeline; using FileTime.Providers.Local; using FileTime.Providers.Sftp; @@ -21,6 +22,7 @@ namespace FileTime.App.Core .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddLocalServices() .AddSmbServices() .AddSftpServices() diff --git a/src/Core/FileTime.Core/Command/Copy/CalculateOperation.cs b/src/Core/FileTime.Core/Command/Copy/CalculateOperation.cs index 94325a4..56925c2 100644 --- a/src/Core/FileTime.Core/Command/Copy/CalculateOperation.cs +++ b/src/Core/FileTime.Core/Command/Copy/CalculateOperation.cs @@ -34,7 +34,7 @@ namespace FileTime.Core.Command.Copy } var resolvedFrom = await from.ResolveAsync(); - operationsByFolder.Add(new OperationProgress(from.Path, resolvedFrom is IElement element ? await element.GetElementSize() : 0L)); + operationsByFolder.Add(new OperationProgress(from.Path, resolvedFrom is IElement element ? await element.GetElementSize() ?? 0L : 0L)); } public Task CreateContainerAsync(IContainer target, string name) diff --git a/src/Core/FileTime.Core/Components/Tab.cs b/src/Core/FileTime.Core/Components/Tab.cs index 82f4e21..c7f6f24 100644 --- a/src/Core/FileTime.Core/Components/Tab.cs +++ b/src/Core/FileTime.Core/Components/Tab.cs @@ -1,5 +1,7 @@ using AsyncEvent; +using FileTime.Core.Helper; using FileTime.Core.Models; +using FileTime.Core.Search; namespace FileTime.Core.Components { @@ -97,7 +99,7 @@ namespace FileTime.Core.Components } _currentSelectedItem = itemToSelect; - _lastPath = GetCommonPath(_lastPath, itemToSelect?.FullName); + _lastPath = PathHelper.GetLongerPath(_lastPath, itemToSelect?.FullName); CurrentSelectedIndex = await GetItemIndex(itemToSelect, CancellationToken.None); @@ -122,7 +124,6 @@ namespace FileTime.Core.Components return null; } - var itemNameToSelect = _lastPath .Split(Constants.SeparatorChar) .Skip((containerFullName?.Split(Constants.SeparatorChar).Length) ?? 0) @@ -131,36 +132,6 @@ namespace FileTime.Core.Components return (await container.GetItems())?.FirstOrDefault(i => i.Name == itemNameToSelect); } - private static string GetCommonPath(string? oldPath, string? newPath) - { - var oldPathParts = oldPath?.Split(Constants.SeparatorChar) ?? Array.Empty(); - var newPathParts = newPath?.Split(Constants.SeparatorChar) ?? Array.Empty(); - - var commonPathParts = new List(); - - var max = oldPathParts.Length > newPathParts.Length ? oldPathParts.Length : newPathParts.Length; - - for (var i = 0; i < max; i++) - { - if (newPathParts.Length <= i) - { - commonPathParts.AddRange(oldPathParts.Skip(i)); - break; - } - else if (oldPathParts.Length <= i || oldPathParts[i] != newPathParts[i]) - { - commonPathParts.AddRange(newPathParts.Skip(i)); - break; - } - else if (oldPathParts[i] == newPathParts[i]) - { - commonPathParts.Add(oldPathParts[i]); - } - } - - return string.Join(Constants.SeparatorChar, commonPathParts); - } - private async Task HandleCurrentLocationRefresh(object? sender, AsyncEventArgs e, CancellationToken token = default) { var currentSelectedName = (await GetCurrentSelectedItem())?.FullName ?? (await GetItemByLastPath())?.FullName; @@ -311,12 +282,24 @@ namespace FileTime.Core.Components public async Task Open() { - var currentLocationItems = (await (await GetCurrentLocation()).GetItems())!; - if (_currentSelectedItem is IContainer childContainer) + if (_currentSelectedItem is ChildSearchElement searchElement && searchElement.GetParent() is IContainer parentContainer) { - if (await GetCurrentLocation() is VirtualContainer currentVirtuakContainer) + await OpenContainer(parentContainer); + var elementToSelect = await (await GetCurrentLocation()).GetByPath(searchElement.DisplayName); + if (elementToSelect != null) { - await SetCurrentLocation(currentVirtuakContainer.CloneVirtualChainFor(childContainer, v => v.IsPermanent)); + await SetCurrentSelectedItem(elementToSelect); + } + } + else if (_currentSelectedItem is ChildSearchContainer searchContainer) + { + await OpenContainer(searchContainer.BaseContainer); + } + else if (_currentSelectedItem is IContainer childContainer) + { + if (await GetCurrentLocation() is VirtualContainer currentVirtualContainer) + { + await SetCurrentLocation(currentVirtualContainer.CloneVirtualChainFor(childContainer, v => v.IsPermanent)); } else { diff --git a/src/Core/FileTime.Core/FileTime.Core.csproj b/src/Core/FileTime.Core/FileTime.Core.csproj index 4af6e49..e8c7c35 100644 --- a/src/Core/FileTime.Core/FileTime.Core.csproj +++ b/src/Core/FileTime.Core/FileTime.Core.csproj @@ -2,6 +2,7 @@ + diff --git a/src/Core/FileTime.Core/Helper/PathHelper.cs b/src/Core/FileTime.Core/Helper/PathHelper.cs new file mode 100644 index 0000000..351b8e4 --- /dev/null +++ b/src/Core/FileTime.Core/Helper/PathHelper.cs @@ -0,0 +1,55 @@ +using FileTime.Core.Models; + +namespace FileTime.Core.Helper +{ + public static class PathHelper + { + public static string GetLongerPath(string? oldPath, string? newPath) + { + var oldPathParts = oldPath?.Split(Constants.SeparatorChar) ?? Array.Empty(); + var newPathParts = newPath?.Split(Constants.SeparatorChar) ?? Array.Empty(); + + var commonPathParts = new List(); + + var max = oldPathParts.Length > newPathParts.Length ? oldPathParts.Length : newPathParts.Length; + + for (var i = 0; i < max; i++) + { + if (newPathParts.Length <= i) + { + commonPathParts.AddRange(oldPathParts.Skip(i)); + break; + } + else if (oldPathParts.Length <= i || oldPathParts[i] != newPathParts[i]) + { + commonPathParts.AddRange(newPathParts.Skip(i)); + break; + } + else if (oldPathParts[i] == newPathParts[i]) + { + commonPathParts.Add(oldPathParts[i]); + } + } + + return string.Join(Constants.SeparatorChar, commonPathParts); + } + public static string GetCommonPath(string? path1, string? path2) + { + var path1Parts = path1?.Split(Constants.SeparatorChar) ?? Array.Empty(); + var path2Parts = path2?.Split(Constants.SeparatorChar) ?? Array.Empty(); + + var commonPathParts = new List(); + + var max = path1Parts.Length > path2Parts.Length ? path2Parts.Length : path1Parts.Length; + + for (var i = 0; i < max; i++) + { + if (path1Parts[i] != path2Parts[i]) break; + + commonPathParts.Add(path1Parts[i]); + } + + return string.Join(Constants.SeparatorChar, commonPathParts); + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Models/ContainerEscapeResult.cs b/src/Core/FileTime.Core/Models/ContainerEscapeResult.cs new file mode 100644 index 0000000..ee0e670 --- /dev/null +++ b/src/Core/FileTime.Core/Models/ContainerEscapeResult.cs @@ -0,0 +1,18 @@ +namespace FileTime.Core.Models +{ + public class ContainerEscapeResult + { + public bool Handled { get; } + public IContainer? NavigateTo { get; } + + public ContainerEscapeResult(bool handled) + { + Handled = handled; + } + + public ContainerEscapeResult(IContainer navigateTo) + { + NavigateTo = navigateTo; + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Models/IContainer.cs b/src/Core/FileTime.Core/Models/IContainer.cs index 2a4285e..ca6c7a9 100644 --- a/src/Core/FileTime.Core/Models/IContainer.cs +++ b/src/Core/FileTime.Core/Models/IContainer.cs @@ -9,6 +9,9 @@ namespace FileTime.Core.Models Task?> GetContainers(CancellationToken token = default); Task?> GetElements(CancellationToken token = default); bool AllowRecursiveDeletion { get; } + bool UseLazyLoad { get; } + bool LazyLoading { get; } + bool CanHandleEscape { get; } Task RefreshAsync(CancellationToken token = default); public async Task GetByPath(string path, bool acceptDeepestMatch = false) @@ -46,9 +49,12 @@ namespace FileTime.Core.Models Task CanOpenAsync(); void Unload(); + Task HandleEscape(); + bool IsLoaded { get; } bool SupportsDirectoryLevelSoftDelete { get; } AsyncEventHandler Refreshed { get; } + AsyncEventHandler LazyLoadingChanged { get; } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Models/IElement.cs b/src/Core/FileTime.Core/Models/IElement.cs index 5b04156..1a68ddb 100644 --- a/src/Core/FileTime.Core/Models/IElement.cs +++ b/src/Core/FileTime.Core/Models/IElement.cs @@ -7,7 +7,7 @@ namespace FileTime.Core.Models bool IsSpecial { get; } string GetPrimaryAttributeText(); Task GetContent(CancellationToken token = default); - Task GetElementSize(CancellationToken token = default); + Task GetElementSize(CancellationToken token = default); Task GetContentReaderAsync(); Task GetContentWriterAsync(); diff --git a/src/Core/FileTime.Core/Models/IItem.cs b/src/Core/FileTime.Core/Models/IItem.cs index a8cf7f6..afc7b82 100644 --- a/src/Core/FileTime.Core/Models/IItem.cs +++ b/src/Core/FileTime.Core/Models/IItem.cs @@ -5,6 +5,7 @@ namespace FileTime.Core.Models public interface IItem { string Name { get; } + string DisplayName { get; } string? FullName { get; } string? NativePath { get; } bool IsHidden { get; } diff --git a/src/GuiApp/FileTime.Avalonia/Models/ItemNamePart.cs b/src/Core/FileTime.Core/Models/ItemNamePart.cs similarity index 87% rename from src/GuiApp/FileTime.Avalonia/Models/ItemNamePart.cs rename to src/Core/FileTime.Core/Models/ItemNamePart.cs index ccfbd62..466ec36 100644 --- a/src/GuiApp/FileTime.Avalonia/Models/ItemNamePart.cs +++ b/src/Core/FileTime.Core/Models/ItemNamePart.cs @@ -1,4 +1,4 @@ -namespace FileTime.Avalonia.Models +namespace FileTime.Core.Models { public class ItemNamePart { diff --git a/src/Core/FileTime.Core/Models/VirtualContainer.cs b/src/Core/FileTime.Core/Models/VirtualContainer.cs index 0b81160..07bb98e 100644 --- a/src/Core/FileTime.Core/Models/VirtualContainer.cs +++ b/src/Core/FileTime.Core/Models/VirtualContainer.cs @@ -20,6 +20,7 @@ namespace FileTime.Core.Models public IReadOnlyList? Elements { get; private set; } public string Name => BaseContainer.Name; + public string DisplayName => BaseContainer.DisplayName; public string? FullName => BaseContainer.FullName; public string? NativePath => BaseContainer.NativePath; @@ -40,6 +41,13 @@ namespace FileTime.Core.Models public bool IsExists => BaseContainer.IsExists; public bool AllowRecursiveDeletion => BaseContainer.AllowRecursiveDeletion; + public bool UseLazyLoad => BaseContainer.UseLazyLoad; + + public bool LazyLoading => BaseContainer.LazyLoading; + public AsyncEventHandler LazyLoadingChanged { get; protected set; } = new(); + + public bool CanHandleEscape => BaseContainer.CanHandleEscape; + private void RefreshAddBase(Func handler) { BaseContainer.Refreshed.Add(handler); @@ -183,5 +191,6 @@ namespace FileTime.Core.Models { BaseContainer.Unload(); } + public async Task HandleEscape() => await BaseContainer.HandleEscape(); } } \ 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 fd4637f..6411fc8 100644 --- a/src/Core/FileTime.Core/Providers/AbstractContainer.cs +++ b/src/Core/FileTime.Core/Providers/AbstractContainer.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using AsyncEvent; using FileTime.Core.Models; @@ -18,8 +19,10 @@ namespace FileTime.Core.Providers public bool SupportsDirectoryLevelSoftDelete { get; protected set; } public AsyncEventHandler Refreshed { get; protected set; } = new(); + public AsyncEventHandler LazyLoadingChanged { get; protected set; } = new(); public string Name { get; protected set; } + public virtual string DisplayName { get; protected set; } public string? FullName { get; protected set; } @@ -41,6 +44,12 @@ namespace FileTime.Core.Providers public virtual bool AllowRecursiveDeletion { get; protected set; } + public bool UseLazyLoad { get; protected set; } + + public bool LazyLoading { get; protected set; } + + public bool CanHandleEscape { get; protected set; } + protected AbstractContainer(TProvider provider, IContainer parent, string name) : this(name) { _parent = parent; @@ -56,7 +65,7 @@ namespace FileTime.Core.Providers private AbstractContainer(string name) { - Name = name; + DisplayName = Name = name; Exceptions = _exceptions.AsReadOnly(); Provider = null!; } @@ -152,5 +161,7 @@ namespace FileTime.Core.Providers } protected void AddException(Exception e) => _exceptions.Add(e); + + public virtual Task HandleEscape() => Task.FromResult(new ContainerEscapeResult(false)); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Providers/AbstractElement.cs b/src/Core/FileTime.Core/Providers/AbstractElement.cs new file mode 100644 index 0000000..891bee3 --- /dev/null +++ b/src/Core/FileTime.Core/Providers/AbstractElement.cs @@ -0,0 +1,58 @@ +using FileTime.Core.Models; + +namespace FileTime.Core.Providers +{ + public abstract class AbstractElement : IElement where TProvider : class, IContentProvider + { + private readonly IContainer _parent; + public bool IsSpecial { get; protected set; } + + public string Name { get; protected set; } + + public string DisplayName { get; protected set; } + + public string? FullName { get; protected set; } + + public string? NativePath { get; protected set; } + + public bool IsHidden { get; protected set; } + + public bool IsDestroyed { get; protected set; } + + public virtual bool IsExists { get; protected set; } + + public virtual SupportsDelete CanDelete { get; protected set; } + + public virtual bool CanRename { get; protected set; } + + public TProvider Provider { get; } + + IContentProvider IItem.Provider => Provider; + + protected AbstractElement(TProvider provider, IContainer parent, string name) + { + _parent = parent; + Provider = provider; + DisplayName = Name = name; + FullName = parent.FullName + Constants.SeparatorChar + name; + IsExists = true; + } + + public abstract Task Delete(bool hardDelete = false); + + public virtual void Destroy() { } + + public abstract Task GetContent(CancellationToken token = default); + + public abstract Task GetContentReaderAsync(); + + public abstract Task GetContentWriterAsync(); + + public abstract Task GetElementSize(CancellationToken token = default); + + public IContainer? GetParent() => _parent; + public abstract string GetPrimaryAttributeText(); + + public abstract Task Rename(string newName); + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Providers/TopContainer.cs b/src/Core/FileTime.Core/Providers/TopContainer.cs index aee89b4..c1f6872 100644 --- a/src/Core/FileTime.Core/Providers/TopContainer.cs +++ b/src/Core/FileTime.Core/Providers/TopContainer.cs @@ -12,6 +12,7 @@ namespace FileTime.Core.Providers #pragma warning disable CS8603 // Possible null reference return. public string Name => null; + public string DisplayName => null; #pragma warning restore CS8603 // Possible null reference return. public string? FullName => null; @@ -36,6 +37,12 @@ namespace FileTime.Core.Providers public bool IsExists => true; public bool AllowRecursiveDeletion => false; + public bool UseLazyLoad => false; + + public bool LazyLoading => false; + public bool CanHandleEscape => false; + public AsyncEventHandler LazyLoadingChanged { get; protected set; } = new(); + public TopContainer(IEnumerable contentProviders) { _contentProviders = new List(contentProviders); @@ -75,5 +82,6 @@ namespace FileTime.Core.Providers public void Destroy() { } public void Unload() { } + public Task HandleEscape() => Task.FromResult(new ContainerEscapeResult(false)); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Search/ChildSearchContainer.cs b/src/Core/FileTime.Core/Search/ChildSearchContainer.cs new file mode 100644 index 0000000..554b6a6 --- /dev/null +++ b/src/Core/FileTime.Core/Search/ChildSearchContainer.cs @@ -0,0 +1,40 @@ +using AsyncEvent; +using FileTime.Core.Models; +using FileTime.Core.Providers; + +namespace FileTime.Core.Search +{ + public class ChildSearchContainer : AbstractContainer + { + public ChildSearchContainer(SearchContainer searchContainer, IContentProvider provider, IContainer baseContainer, string name, string displayName, List searchDisplayName) : base(provider, baseContainer.GetParent()!, name) + { + DisplayName = displayName; + SearchContainer = searchContainer; + SearchDisplayName = searchDisplayName; + BaseContainer = baseContainer; + } + + public override bool IsExists => true; + + public SearchContainer SearchContainer { get; } + public List SearchDisplayName { get; } + public IContainer BaseContainer { get; } + + public override async Task RefreshAsync(CancellationToken token = default) + { + if (Refreshed != null) await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty, token); + } + + public override Task CloneAsync() => throw new NotImplementedException(); + + 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 Task> RefreshItems(CancellationToken token = default) => throw new NotImplementedException(); + + public override Task Rename(string newName) => throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Search/ChildSearchElement.cs b/src/Core/FileTime.Core/Search/ChildSearchElement.cs new file mode 100644 index 0000000..08e1494 --- /dev/null +++ b/src/Core/FileTime.Core/Search/ChildSearchElement.cs @@ -0,0 +1,33 @@ +using FileTime.Core.Models; +using FileTime.Core.Providers; + +namespace FileTime.Core.Search +{ + public class ChildSearchElement : AbstractElement + { + public ChildSearchElement(SearchContainer searchContainer, IContentProvider provider, IContainer parent, string name, string displayName, List searchDisplayName) : base(provider, parent, name) + { + DisplayName = displayName; + NativePath = FullName; + SearchContainer = searchContainer; + SearchDisplayName = searchDisplayName; + } + + public SearchContainer SearchContainer { get; } + public List SearchDisplayName { get; } + + public override Task Delete(bool hardDelete = false) => throw new NotSupportedException(); + + public override Task GetContent(CancellationToken token = default) => throw new NotSupportedException(); + + public override Task GetContentReaderAsync() => throw new NotSupportedException(); + + public override Task GetContentWriterAsync() => throw new NotSupportedException(); + + public override Task GetElementSize(CancellationToken token = default) => Task.FromResult((long?)null); + + 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/Search/NameRegexSearchTask.cs b/src/Core/FileTime.Core/Search/NameRegexSearchTask.cs new file mode 100644 index 0000000..f07eac5 --- /dev/null +++ b/src/Core/FileTime.Core/Search/NameRegexSearchTask.cs @@ -0,0 +1,17 @@ +using System.Text.RegularExpressions; +using FileTime.Core.Models; + +namespace FileTime.Core.Search +{ + public class NameRegexSearchTask : SearchTaskBase + { + private readonly Regex _nameRegex; + + public NameRegexSearchTask(string namePattern, IContainer searchBaseContainer) : base(searchBaseContainer) + { + _nameRegex = new Regex(namePattern); + } + + protected override Task IsItemMatch(IItem item) => Task.FromResult(_nameRegex.IsMatch(item.Name)); + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Search/NameSearchTask.cs b/src/Core/FileTime.Core/Search/NameSearchTask.cs new file mode 100644 index 0000000..a2c28b2 --- /dev/null +++ b/src/Core/FileTime.Core/Search/NameSearchTask.cs @@ -0,0 +1,21 @@ +using FileTime.Core.Models; +using FileTime.Core.Services; + +namespace FileTime.Core.Search +{ + public class NameSearchTask : SearchTaskBase + { + private readonly string _name; + private readonly ItemNameConverterService _itemNameConverterService; + + public NameSearchTask(string name, IContainer searchBaseContainer, ItemNameConverterService itemNameConverterService) : base(searchBaseContainer) + { + _name = name; + _itemNameConverterService = itemNameConverterService; + } + + protected override Task IsItemMatch(IItem item) => Task.FromResult(item.Name.Contains(_name)); + + public override List GetDisplayName(IItem item) => _itemNameConverterService.GetDisplayName(item, _name); + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Search/SearchContainer.cs b/src/Core/FileTime.Core/Search/SearchContainer.cs new file mode 100644 index 0000000..1c6c372 --- /dev/null +++ b/src/Core/FileTime.Core/Search/SearchContainer.cs @@ -0,0 +1,103 @@ +using System.Threading.Tasks; +using AsyncEvent; +using FileTime.Core.Models; +using FileTime.Core.Providers; + +namespace FileTime.Core.Search +{ + public class SearchContainer : AbstractContainer + { + private int _childContainerCounter; + private int _childElementCounter; + private readonly List _containers; + private IReadOnlyList _items; + private readonly List _elements; + + private readonly IReadOnlyList _containersReadOnly; + private readonly IReadOnlyList _elementsReadOnly; + private readonly SearchTaskBase _searchTaskBase; + + public IContainer SearchBaseContainer { get; } + public override bool IsExists => throw new NotImplementedException(); + + public SearchContainer(IContainer searchBaseContainer, SearchTaskBase searchTaskBase) : base(searchBaseContainer.Provider, searchBaseContainer.GetParent()!, searchBaseContainer.Name) + { + SearchBaseContainer = searchBaseContainer; + _containers = new List(); + _elements = new List(); + _searchTaskBase = searchTaskBase; + + _containersReadOnly = _containers.AsReadOnly(); + _elementsReadOnly = _elements.AsReadOnly(); + _items = _containers.Cast().Concat(_elements).ToList().AsReadOnly(); + + UseLazyLoad = true; + CanHandleEscape = true; + } + + 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 async Task AddContainer(IContainer container) + { + var childContainer = new ChildSearchContainer(this, Provider, container, "container" + _childContainerCounter++, container.DisplayName, _searchTaskBase.GetDisplayName(container)); + _containers.Add(childContainer); + await UpdateChildren(); + } + + public async Task AddElement(IElement element) + { + var childElement = new ChildSearchElement(this, Provider, element.GetParent()!, "element" + _childElementCounter++, element.DisplayName, _searchTaskBase.GetDisplayName(element)); + _elements.Add(childElement); + 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?> 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); + + 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 Task> RefreshItems(CancellationToken token = default) => throw new NotImplementedException(); + + public override Task Rename(string newName) => throw new NotSupportedException(); + + public override Task HandleEscape() + { + if (_searchTaskBase.Cancel()) return Task.FromResult(new ContainerEscapeResult(true)); + + return Task.FromResult(new ContainerEscapeResult(SearchBaseContainer)); + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core/Search/SearchTaskBase.cs b/src/Core/FileTime.Core/Search/SearchTaskBase.cs new file mode 100644 index 0000000..fab6ea8 --- /dev/null +++ b/src/Core/FileTime.Core/Search/SearchTaskBase.cs @@ -0,0 +1,127 @@ +using FileTime.Core.Models; + +namespace FileTime.Core.Search +{ + public abstract class SearchTaskBase + { + private readonly object _searchGuard = new(); + private readonly IContainer _baseContainer; + private CancellationTokenSource? _cancellationTokenSource; + + public SearchContainer TargetContainer { get; } + public bool Searching { get; private set; } + + protected SearchTaskBase(IContainer searchBaseContainer) + { + TargetContainer = new SearchContainer(searchBaseContainer, this); + _baseContainer = searchBaseContainer; + } + + public void Start() + { + lock (_searchGuard) + { + if (Searching) return; + Searching = true; + try + { + _cancellationTokenSource?.Cancel(); + } + catch { } + } + + new Thread(BootstrapSearch).Start(); + + void BootstrapSearch() + { + try + { + Task.Run(Search).Wait(); + } + finally + { + lock (_searchGuard) + { + Searching = false; + } + } + } + } + + private async Task Search() + { + try + { + using (_cancellationTokenSource = new CancellationTokenSource()) + { + await TargetContainer.RunWithLazyLoading(async (token) => await TraverseTree(_baseContainer, token), _cancellationTokenSource.Token); + } + } + finally + { + _cancellationTokenSource = null; + } + } + + private async Task TraverseTree(IContainer container, CancellationToken token = default) + { + if (token.IsCancellationRequested) return; + + await container.RefreshAsync(token); + if (await IsItemMatch(container)) + { + await AddContainer(container); + } + var childElements = await container.GetElements(token); + var childContainers = await container.GetContainers(token); + + if (childElements != null) + { + await foreach (var childElement in + childElements + .ToAsyncEnumerable() + .WhereAwait(async e => await IsItemMatch(e)) + ) + { + if (token.IsCancellationRequested) return; + await AddElement(childElement); + } + } + + if (childContainers != null) + { + foreach (var childContainer in childContainers) + { + await TraverseTree(childContainer, token); + } + } + } + + private async Task AddContainer(IContainer container) + { + await TargetContainer.AddContainer(container); + } + + private async Task AddElement(IElement element) + { + await TargetContainer.AddElement(element); + } + + protected abstract Task IsItemMatch(IItem item); + + public virtual List GetDisplayName(IItem item) + { + return new List() + { + new ItemNamePart(item.Name) + }; + } + + public bool Cancel() + { + if (!Searching || _cancellationTokenSource == null) return false; + _cancellationTokenSource?.Cancel(); + return true; + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Services/ItemNameConverterService.cs b/src/Core/FileTime.Core/Services/ItemNameConverterService.cs similarity index 60% rename from src/GuiApp/FileTime.Avalonia/Services/ItemNameConverterService.cs rename to src/Core/FileTime.Core/Services/ItemNameConverterService.cs index f77b3b8..f9f3d97 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/ItemNameConverterService.cs +++ b/src/Core/FileTime.Core/Services/ItemNameConverterService.cs @@ -1,34 +1,25 @@ -using Avalonia.Media; -using FileTime.Avalonia.Application; -using FileTime.Avalonia.Models; -using FileTime.Avalonia.ViewModels; -using FileTime.Core.Models; -using MvvmGen; -using System; -using System.Collections.Generic; +using FileTime.Core.Models; -namespace FileTime.Avalonia.Services +namespace FileTime.Core.Services { - [ViewModel] - [Inject(typeof(AppState))] - public partial class ItemNameConverterService + public class ItemNameConverterService { - public List GetDisplayName(IItemViewModel itemViewModel) + public List GetDisplayName(IItem item, string? searchText) { var nameParts = new List(); - var rapidTravelText = AppState.RapidTravelText.ToLower(); + searchText = searchText?.ToLower(); - var name = itemViewModel.Item is IElement ? GetFileName(itemViewModel.Item.Name) : itemViewModel.Item.Name; - if (AppState.ViewMode == ViewMode.RapidTravel && rapidTravelText.Length > 0) + var name = item is IElement ? GetFileName(item.DisplayName) : item.DisplayName; + if (!string.IsNullOrEmpty(searchText)) { var nameLeft = name; - while (nameLeft.ToLower().IndexOf(rapidTravelText, StringComparison.Ordinal) is int rapidTextStart && rapidTextStart != -1) + while (nameLeft.ToLower().IndexOf(searchText, StringComparison.Ordinal) is int rapidTextStart && rapidTextStart != -1) { var before = rapidTextStart > 0 ? nameLeft.Substring(0, rapidTextStart) : null; - var rapidTravel = nameLeft.Substring(rapidTextStart, rapidTravelText.Length); + var rapidTravel = nameLeft.Substring(rapidTextStart, searchText.Length); - nameLeft = nameLeft.Substring(rapidTextStart + rapidTravelText.Length); + nameLeft = nameLeft.Substring(rapidTextStart + searchText.Length); if (before != null) { diff --git a/src/Core/FileTime.Core/Timeline/TimeContainer.cs b/src/Core/FileTime.Core/Timeline/TimeContainer.cs index e5e7db9..bab44cc 100644 --- a/src/Core/FileTime.Core/Timeline/TimeContainer.cs +++ b/src/Core/FileTime.Core/Timeline/TimeContainer.cs @@ -14,6 +14,7 @@ namespace FileTime.Core.Timeline public AsyncEventHandler Refreshed { get; } = new AsyncEventHandler(); public string Name { get; } + public string DisplayName { get; } public string? FullName { get; } @@ -36,12 +37,18 @@ namespace FileTime.Core.Timeline public bool IsExists => true; public bool AllowRecursiveDeletion => true; + public bool UseLazyLoad => false; + + public bool LazyLoading => false; + public bool CanHandleEscape => false; + public AsyncEventHandler LazyLoadingChanged { get; protected set; } = new(); + public TimeContainer(string name, IContainer parent, IContentProvider contentProvider, IContentProvider virtualContentProvider, PointInTime pointInTime) { _parent = parent; _pointInTime = pointInTime; - Name = name; + DisplayName = Name = name; Provider = contentProvider; VirtualProvider = virtualContentProvider; FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name; @@ -131,5 +138,6 @@ namespace FileTime.Core.Timeline public void Destroy() => IsDestroyed = true; public void Unload() { } + public Task HandleEscape() => Task.FromResult(new ContainerEscapeResult(false)); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/TimeElement.cs b/src/Core/FileTime.Core/Timeline/TimeElement.cs index e5224e5..61b704a 100644 --- a/src/Core/FileTime.Core/Timeline/TimeElement.cs +++ b/src/Core/FileTime.Core/Timeline/TimeElement.cs @@ -10,7 +10,7 @@ namespace FileTime.Core.Timeline { _parent = parent; - Name = name; + DisplayName = Name = name; FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name; Provider = contentProvider; VirtualProvider = virtualContentProvider; @@ -19,6 +19,7 @@ namespace FileTime.Core.Timeline public bool IsSpecial => false; public string Name { get; } + public string DisplayName { get; } public string? FullName { get; } @@ -46,7 +47,7 @@ namespace FileTime.Core.Timeline public Task Rename(string newName) => Task.CompletedTask; public Task GetContent(CancellationToken token = default) => Task.FromResult(""); - public Task GetElementSize(CancellationToken token = default) => Task.FromResult(-1L); + public Task GetElementSize(CancellationToken token = default) => Task.FromResult((long?)-1L); public void Destroy() => IsDestroyed = true; diff --git a/src/GuiApp/FileTime.Avalonia/App.axaml b/src/GuiApp/FileTime.Avalonia/App.axaml index 1503821..38c392d 100644 --- a/src/GuiApp/FileTime.Avalonia/App.axaml +++ b/src/GuiApp/FileTime.Avalonia/App.axaml @@ -223,5 +223,18 @@ + + diff --git a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs index 8528c2e..fdc77af 100644 --- a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs +++ b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs @@ -2,7 +2,6 @@ using FileTime.Core.Components; using FileTime.Core.Models; using FileTime.Providers.Local; -using FileTime.Avalonia.Services; using FileTime.Avalonia.ViewModels; using MvvmGen; using System; @@ -12,14 +11,20 @@ using System.Threading.Tasks; using FileTime.App.Core.Tab; using System.Threading; using FileTime.Core.Timeline; +using FileTime.Avalonia.ViewModels.ItemPreview; +using FileTime.Core.Search; +using Microsoft.Extensions.DependencyInjection; +using FileTime.Core.Services; namespace FileTime.Avalonia.Application { [ViewModel] + [Inject(typeof(AppState))] [Inject(typeof(ItemNameConverterService))] [Inject(typeof(LocalContentProvider))] [Inject(typeof(Tab))] [Inject(typeof(TimeRunner), propertyName: "_timeRunner")] + [Inject(typeof(IServiceProvider), PropertyName = "_serviceProvider")] public partial class TabContainer : INewItemProcessor { private bool _updateFromCode; @@ -63,7 +68,7 @@ namespace FileTime.Avalonia.Application } [Property] - private ElementPreviewViewModel? _elementPreview; + private IItemPreviewViewModel? _itemPreview; public async Task SetSelectedItemAsync(IItemViewModel? value) { @@ -104,7 +109,7 @@ namespace FileTime.Avalonia.Application var currentLocation = await Tab.GetCurrentLocation(); var parent = GenerateParent(currentLocation); - CurrentLocation = new ContainerViewModel(this, parent, currentLocation, ItemNameConverterService); + CurrentLocation = new ContainerViewModel(this, parent, currentLocation, ItemNameConverterService, AppState); await CurrentLocation.Init(); if (parent != null) @@ -127,7 +132,7 @@ namespace FileTime.Avalonia.Application if (parentContainer == null) return null; var parentParent = recursive ? GenerateParent(parentContainer.GetParent(), recursive) : null; - var parent = new ContainerViewModel(this, parentParent, parentContainer, ItemNameConverterService); + var parent = new ContainerViewModel(this, parentParent, parentContainer, ItemNameConverterService, AppState); parentParent?.ChildrenToAdopt.Add(parent); return parent; } @@ -136,7 +141,7 @@ namespace FileTime.Avalonia.Application { var currentLocation = await Tab.GetCurrentLocation(token); var parent = GenerateParent(currentLocation); - CurrentLocation = new ContainerViewModel(this, parent, currentLocation, ItemNameConverterService); + CurrentLocation = new ContainerViewModel(this, parent, currentLocation, ItemNameConverterService, AppState); await CurrentLocation.Init(token: token); if (token.IsCancellationRequested) return; @@ -179,7 +184,7 @@ namespace FileTime.Avalonia.Application if (currentSelectenItem is ContainerViewModel currentSelectedContainer) { await SetSelectedItemAsync(currentSelectedContainer); - newChildContainer = currentSelectedContainer; + newChildContainer = currentSelectenItem is ChildSearchContainer ? null : currentSelectedContainer; } else if (currentSelectenItem is ElementViewModel element) { @@ -205,16 +210,30 @@ namespace FileTime.Avalonia.Application ChildContainer = newChildContainer; - if (currentSelectenItem is ElementViewModel elementViewModel) + IItemPreviewViewModel? preview = null; + if (currentSelectenItem == null) + { + + } + else if (currentSelectenItem.Item is ChildSearchContainer searchContainer) + { + var searchContainerPreview = _serviceProvider.GetService()!; + await searchContainerPreview.Init(searchContainer, _currentLocation.Container); + preview = searchContainerPreview; + } + else if (currentSelectenItem.Item is ChildSearchElement searchElement) + { + var searchElementPreview = _serviceProvider.GetService()!; + await searchElementPreview.Init(searchElement, _currentLocation.Container); + preview = searchElementPreview; + } + else if (currentSelectenItem is ElementViewModel elementViewModel) { var elementPreview = new ElementPreviewViewModel(); await elementPreview.Init(elementViewModel.Element); - ElementPreview = elementPreview; - } - else - { - ElementPreview = null; + preview = elementPreview; } + ItemPreview = preview; /*} catch { @@ -310,10 +329,7 @@ namespace FileTime.Avalonia.Application public async Task Open() { - if (ChildContainer != null) - { - await RunFromCode(Tab.Open); - } + await RunFromCode(Tab.Open); } public async Task GoUp() diff --git a/src/GuiApp/FileTime.Avalonia/Assets/loading.svg b/src/GuiApp/FileTime.Avalonia/Assets/loading.svg new file mode 100644 index 0000000..5f0594a --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Assets/loading.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs b/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs index a76eab3..b9da353 100644 --- a/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs +++ b/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs @@ -55,6 +55,8 @@ namespace FileTime.Avalonia.Configuration new CommandBindingConfiguration(Commands.Cut, new[] { Key.D, Key.D }), new CommandBindingConfiguration(Commands.Edit, new KeyConfig(Key.F4)), new CommandBindingConfiguration(Commands.EnterRapidTravel, new KeyConfig(Key.OemComma, shift: true)), + new CommandBindingConfiguration(Commands.FindByName, new[] { Key.F, Key.N }), + new CommandBindingConfiguration(Commands.FindByNameRegex, new[] { Key.F, Key.R }), new CommandBindingConfiguration(Commands.GoToHome, new[] { Key.G, Key.H }), new CommandBindingConfiguration(Commands.GoToPath, new KeyConfig(Key.OemComma, ctrl: true)), new CommandBindingConfiguration(Commands.GoToPath, new[] { Key.G, Key.P }), diff --git a/src/GuiApp/FileTime.Avalonia/Converters/GetFileExtensionConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/GetFileExtensionConverter.cs index c543cee..9cabcf3 100644 --- a/src/GuiApp/FileTime.Avalonia/Converters/GetFileExtensionConverter.cs +++ b/src/GuiApp/FileTime.Avalonia/Converters/GetFileExtensionConverter.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using Avalonia.Data.Converters; -using FileTime.Avalonia.Services; +using FileTime.Core.Services; using Microsoft.Extensions.DependencyInjection; namespace FileTime.Avalonia.Converters diff --git a/src/GuiApp/FileTime.Avalonia/Converters/NamePartShrinkerConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/NamePartShrinkerConverter.cs index 2c17e73..4f9adab 100644 --- a/src/GuiApp/FileTime.Avalonia/Converters/NamePartShrinkerConverter.cs +++ b/src/GuiApp/FileTime.Avalonia/Converters/NamePartShrinkerConverter.cs @@ -4,8 +4,8 @@ using System.Globalization; using System.Linq; using Avalonia.Data.Converters; using Avalonia.Media; -using FileTime.Avalonia.Models; using FileTime.Avalonia.ViewModels; +using FileTime.Core.Models; namespace FileTime.Avalonia.Converters { diff --git a/src/GuiApp/FileTime.Avalonia/Models/ElementPreviewMode.cs b/src/GuiApp/FileTime.Avalonia/Models/ElementPreviewMode.cs deleted file mode 100644 index 627971d..0000000 --- a/src/GuiApp/FileTime.Avalonia/Models/ElementPreviewMode.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace FileTime.Avalonia.Models -{ - public enum ElementPreviewMode - { - Unknown, - Text, - Empty - } -} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Models/ItemPreviewMode.cs b/src/GuiApp/FileTime.Avalonia/Models/ItemPreviewMode.cs new file mode 100644 index 0000000..b7d9ba6 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Models/ItemPreviewMode.cs @@ -0,0 +1,11 @@ +namespace FileTime.Avalonia.Models +{ + public enum ItemPreviewMode + { + Unknown, + Text, + Empty, + SearchContainer, + SearchElement + } +} \ 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 7d87439..d270503 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs @@ -22,6 +22,8 @@ using FileTime.Core.Components; using FileTime.Core.Interactions; using FileTime.Core.Models; using FileTime.Core.Providers; +using FileTime.Core.Search; +using FileTime.Core.Services; using FileTime.Core.Timeline; using FileTime.Providers.Local; using FileTime.Tools.Compression.Command; @@ -44,6 +46,7 @@ namespace FileTime.Avalonia.Services private readonly Dictionary> _commandHandlers; private readonly ProgramsService _programsService; private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; public CommandHandlerService( AppState appState, @@ -55,7 +58,8 @@ namespace FileTime.Avalonia.Services IIconProvider iconProvider, IEnumerable contentProviders, ProgramsService programsService, - ILogger logger) + ILogger logger, + IServiceProvider serviceProvider) { _appState = appState; _localContentProvider = localContentProvider; @@ -67,6 +71,7 @@ namespace FileTime.Avalonia.Services _contentProviders = contentProviders; _programsService = programsService; _logger = logger; + _serviceProvider = serviceProvider; _commandHandlers = new Dictionary> { @@ -82,6 +87,8 @@ namespace FileTime.Avalonia.Services {Commands.Cut, Cut}, {Commands.Edit, Edit}, {Commands.EnterRapidTravel, EnterRapidTravelMode}, + {Commands.FindByName, FindByName}, + {Commands.FindByNameRegex, FindByNameRegex}, {Commands.GoToHome, GotToHome}, {Commands.GoToPath, GoToContainer}, {Commands.GoToProvider, GotToProvider}, @@ -235,7 +242,7 @@ namespace FileTime.Avalonia.Services var newTab = new Tab(); await newTab.Init(newContainer); - tabContainer = new TabContainer(_timeRunner, newTab, _localContentProvider, _itemNameConverterService); + tabContainer = new TabContainer(_serviceProvider, _timeRunner, newTab, _localContentProvider, _itemNameConverterService, _appState); await tabContainer.Init(number); var i = 0; @@ -940,5 +947,53 @@ namespace FileTime.Avalonia.Services } } } + + private Task FindByName() + { + var handler = async (List inputs) => + { + var container = _appState.SelectedTab.CurrentLocation.Container; + while (container is SearchContainer searchContainer) + { + container = searchContainer.SearchBaseContainer; + } + + var search = new NameSearchTask(inputs[0].Value, container, _itemNameConverterService); + search.Start(); + + await OpenContainer(search.TargetContainer); + }; + + _dialogService.ReadInputs(new List() + { + InputElement.ForText("Item name pattern") + }, handler); + + return Task.CompletedTask; + } + + private Task FindByNameRegex() + { + var handler = async (List inputs) => + { + var container = _appState.SelectedTab.CurrentLocation.Container; + while (container is SearchContainer searchContainer) + { + container = searchContainer.SearchBaseContainer; + } + + var search = new NameRegexSearchTask(inputs[0].Value, container); + search.Start(); + + await OpenContainer(search.TargetContainer); + }; + + _dialogService.ReadInputs(new List() + { + InputElement.ForText("Item name pattern") + }, handler); + + return Task.CompletedTask; + } } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Services/KeyInputHandlerService.cs b/src/GuiApp/FileTime.Avalonia/Services/KeyInputHandlerService.cs index ee32e27..4c8c9fa 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/KeyInputHandlerService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/KeyInputHandlerService.cs @@ -69,11 +69,41 @@ namespace FileTime.Avalonia.Services if (key == Key.Escape) { - _appState.IsAllShortcutVisible = false; - _appState.MessageBoxText = null; - _appState.PreviousKeys.Clear(); - _appState.PossibleCommands = new(); - setHandled(true); + var doGeneralReset = false; + if (_appState.PreviousKeys.Count > 1 || _appState.IsAllShortcutVisible || _appState.MessageBoxText != null) + { + doGeneralReset = true; + } + else if (_appState.SelectedTab.CurrentLocation.Container.CanHandleEscape) + { + var escapeResult = await _appState.SelectedTab.CurrentLocation.Container.HandleEscape(); + if (escapeResult.NavigateTo != null) + { + setHandled(true); + _appState.PreviousKeys.Clear(); + await _appState.SelectedTab.OpenContainer(escapeResult.NavigateTo); + } + else + { + if(escapeResult.Handled) + { + _appState.PreviousKeys.Clear(); + } + else + { + doGeneralReset = true; + } + } + } + + if (doGeneralReset) + { + setHandled(true); + _appState.IsAllShortcutVisible = false; + _appState.MessageBoxText = null; + _appState.PreviousKeys.Clear(); + _appState.PossibleCommands = new(); + } } else if (key == Key.Enter && _appState.MessageBoxText != null) diff --git a/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs b/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs index 6e621ba..8a0ebbe 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs @@ -12,6 +12,7 @@ using FileTime.Providers.Local; using FileTime.Core.Models; using Microsoft.Extensions.Logging; using FileTime.Core.Timeline; +using FileTime.Core.Services; namespace FileTime.Avalonia.Services { @@ -25,6 +26,7 @@ namespace FileTime.Avalonia.Services private readonly LocalContentProvider _localContentProvider; private readonly ILogger _logger; private readonly TimeRunner _timeRunner; + private readonly IServiceProvider _serviceProvider; public StatePersistenceService( AppState appState, @@ -32,7 +34,8 @@ namespace FileTime.Avalonia.Services IEnumerable contentProviders, LocalContentProvider localContentProvider, ILogger logger, - TimeRunner timeRunner) + TimeRunner timeRunner, + IServiceProvider serviceProvider) { _appState = appState; _itemNameConverterService = itemNameConverterService; @@ -40,13 +43,14 @@ namespace FileTime.Avalonia.Services _localContentProvider = localContentProvider; _logger = logger; _settingsPath = Path.Combine(Program.AppDataRoot, "savedState.json"); + _timeRunner = timeRunner; + _serviceProvider = serviceProvider; _jsonOptions = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true, WriteIndented = true }; - this._timeRunner = timeRunner; } public async Task LoadStatesAsync() @@ -153,7 +157,7 @@ namespace FileTime.Avalonia.Services } } - var newTabContainer = new TabContainer(_timeRunner, newTab, _localContentProvider, _itemNameConverterService); + var newTabContainer = new TabContainer(_serviceProvider, _timeRunner, newTab, _localContentProvider, _itemNameConverterService, _appState); await newTabContainer.Init(tab.Number); _appState.Tabs.Add(newTabContainer); } diff --git a/src/GuiApp/FileTime.Avalonia/Startup.cs b/src/GuiApp/FileTime.Avalonia/Startup.cs index 258465a..9483c16 100644 --- a/src/GuiApp/FileTime.Avalonia/Startup.cs +++ b/src/GuiApp/FileTime.Avalonia/Startup.cs @@ -7,6 +7,7 @@ using FileTime.Avalonia.IconProviders; using FileTime.Avalonia.Logging; using FileTime.Avalonia.Services; using FileTime.Avalonia.ViewModels; +using FileTime.Avalonia.ViewModels.ItemPreview; using FileTime.Core.Command; using FileTime.Core.Interactions; using FileTime.Core.Persistence; @@ -23,13 +24,14 @@ namespace FileTime.Avalonia { return serviceCollection .AddTransient() + .AddTransient() + .AddTransient() .AddSingleton(); } internal static IServiceCollection AddServices(this IServiceCollection serviceCollection) { serviceCollection = serviceCollection .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs index 62be724..5fa478e 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs @@ -1,5 +1,6 @@ using AsyncEvent; using FileTime.Core.Models; +using FileTime.Avalonia.Application; using FileTime.Avalonia.Models; using FileTime.Avalonia.Services; using MvvmGen; @@ -11,10 +12,12 @@ using System.Text; using System.Threading.Tasks; using FileTime.Avalonia.Application; using System.Threading; +using FileTime.Core.Services; namespace FileTime.Avalonia.ViewModels { [ViewModel] + [Inject(typeof(AppState))] [Inject(typeof(ItemNameConverterService))] public partial class ContainerViewModel : IItemViewModel { @@ -51,6 +54,8 @@ namespace FileTime.Avalonia.ViewModels public List ChildrenToAdopt { get; } = new List(); + public bool LazyLoading => _container.LazyLoading; + [PropertyInvalidate(nameof(IsSelected))] [PropertyInvalidate(nameof(IsAlternative))] [PropertyInvalidate(nameof(IsMarked))] @@ -65,7 +70,7 @@ namespace FileTime.Avalonia.ViewModels _ => ItemViewMode.Default }; - public List DisplayName => ItemNameConverterService.GetDisplayName(this); + public List DisplayName => ItemNameConverterService.GetDisplayName(Item, AppState.ViewMode == Application.ViewMode.RapidTravel ? AppState.RapidTravelText : null); public Task Containers => GetContainers(); public Task Elements => GetElements(); @@ -116,13 +121,14 @@ namespace FileTime.Avalonia.ViewModels } } - public ContainerViewModel(INewItemProcessor newItemProcessor, ContainerViewModel? parent, IContainer container, ItemNameConverterService itemNameConverterService) : this(itemNameConverterService) + public ContainerViewModel(INewItemProcessor newItemProcessor, ContainerViewModel? parent, IContainer container, ItemNameConverterService itemNameConverterService, AppState appState) : this(itemNameConverterService, appState) { _newItemProcessor = newItemProcessor; Parent = parent; Container = container; Container.Refreshed.Add(Container_Refreshed); + Container.LazyLoadingChanged.Add(Container_LazyLoadingChanged); } public void InvalidateDisplayName() => OnPropertyChanged(nameof(DisplayName)); @@ -138,6 +144,12 @@ namespace FileTime.Avalonia.ViewModels await Refresh(false, false, token: token); } + private Task Container_LazyLoadingChanged(object? sender, bool lazyLoading, CancellationToken token = default) + { + OnPropertyChanged(nameof(LazyLoading)); + return Task.CompletedTask; + } + [Obsolete($"Use the parametrizable version of {nameof(Refresh)}.")] private async Task Refresh() { @@ -161,7 +173,7 @@ namespace FileTime.Avalonia.ViewModels { foreach (var container in containers) { - newContainers.Add(await AdoptOrReuseOrCreateItem(container, alloweReuse, (c2) => new ContainerViewModel(_newItemProcessor, this, c2, ItemNameConverterService))); + newContainers.Add(await AdoptOrReuseOrCreateItem(container, alloweReuse, (c2) => new ContainerViewModel(_newItemProcessor, this, c2, ItemNameConverterService, AppState))); } } @@ -171,7 +183,7 @@ namespace FileTime.Avalonia.ViewModels { var generator = async (IElement e) => { - var element = new ElementViewModel(e, this, ItemNameConverterService); + var element = new ElementViewModel(e, this, ItemNameConverterService, AppState); await element.Init(); return element; }; diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ElementViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ElementViewModel.cs index d436f72..cc33895 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ElementViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ElementViewModel.cs @@ -4,10 +4,13 @@ using FileTime.Avalonia.Services; using MvvmGen; using System.Collections.Generic; using System.Threading.Tasks; +using FileTime.Avalonia.Application; +using FileTime.Core.Services; namespace FileTime.Avalonia.ViewModels { [ViewModel] + [Inject(typeof(AppState))] [Inject(typeof(ItemNameConverterService))] public partial class ElementViewModel : IItemViewModel { @@ -29,7 +32,7 @@ namespace FileTime.Avalonia.ViewModels private ContainerViewModel? _parent; [Property] - private long _size; + private long? _size; [PropertyInvalidate(nameof(IsSelected))] [PropertyInvalidate(nameof(IsAlternative))] @@ -45,9 +48,9 @@ namespace FileTime.Avalonia.ViewModels _ => ItemViewMode.Default }; - public List DisplayName => ItemNameConverterService.GetDisplayName(this); + public List DisplayName => ItemNameConverterService.GetDisplayName(Item, AppState.ViewMode == Application.ViewMode.RapidTravel ? AppState.RapidTravelText : null); - public ElementViewModel(IElement element, ContainerViewModel parent, ItemNameConverterService itemNameConverterService) : this(itemNameConverterService) + public ElementViewModel(IElement element, ContainerViewModel parent, ItemNameConverterService itemNameConverterService, AppState appState) : this(itemNameConverterService, appState) { Element = element; Parent = parent; @@ -57,7 +60,11 @@ namespace FileTime.Avalonia.ViewModels public async Task Init() { - Size = await _element.GetElementSize(); + try + { + Size = await _element.GetElementSize(); + } + catch { } } } } diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ElementPreviewViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/ElementPreviewViewModel.cs similarity index 64% rename from src/GuiApp/FileTime.Avalonia/ViewModels/ElementPreviewViewModel.cs rename to src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/ElementPreviewViewModel.cs index c5f14cc..bad7b70 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ElementPreviewViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/ElementPreviewViewModel.cs @@ -2,16 +2,13 @@ using FileTime.Core.Models; using MvvmGen; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; -namespace FileTime.Avalonia.ViewModels +namespace FileTime.Avalonia.ViewModels.ItemPreview { [ViewModel] - public partial class ElementPreviewViewModel + public partial class ElementPreviewViewModel : IItemPreviewViewModel { private const int MAXTEXTPREVIEWSIZE = 1024 * 1024; [Property] @@ -21,16 +18,23 @@ namespace FileTime.Avalonia.ViewModels private string? _textContent; [Property] - private ElementPreviewMode? _mode; + private ItemPreviewMode _mode = ItemPreviewMode.Unknown; public async Task Init(IElement element, CancellationToken token = default) { Element = element; - var elementSize = await element.GetElementSize(token); + long? elementSize = null; + + try + { + elementSize = await element.GetElementSize(token); + } + catch { } + if (elementSize == 0) { - Mode = ElementPreviewMode.Empty; + Mode = ItemPreviewMode.Empty; } else if (elementSize < MAXTEXTPREVIEWSIZE) { @@ -38,15 +42,15 @@ namespace FileTime.Avalonia.ViewModels { TextContent = await element.GetContent(); } - catch(Exception e) + catch (Exception e) { TextContent = $"Error while getting content of {element.FullName}. " + e.ToString(); } - Mode = ElementPreviewMode.Text; + Mode = ItemPreviewMode.Text; } else { - Mode = ElementPreviewMode.Unknown; + Mode = ItemPreviewMode.Unknown; } } } diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/IItemPreviewViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/IItemPreviewViewModel.cs new file mode 100644 index 0000000..ab00127 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/IItemPreviewViewModel.cs @@ -0,0 +1,9 @@ +using FileTime.Avalonia.Models; + +namespace FileTime.Avalonia.ViewModels.ItemPreview +{ + public interface IItemPreviewViewModel + { + ItemPreviewMode Mode { 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 new file mode 100644 index 0000000..f6644dd --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchContainerPreview.cs @@ -0,0 +1,33 @@ +using Avalonia.Media; +using Avalonia.Threading; +using FileTime.Avalonia.Models; +using FileTime.Core.Helper; +using FileTime.Core.Models; +using FileTime.Core.Search; +using MvvmGen; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace FileTime.Avalonia.ViewModels.ItemPreview +{ + [ViewModel] + public partial class SearchContainerPreview : IItemPreviewViewModel + { + [Property] + private ItemPreviewMode _mode = ItemPreviewMode.SearchContainer; + + [Property] + private List? _itemNameParts; + + [Property] + private string? _realtiveParentPath; + + public async Task Init(ChildSearchContainer container, IContainer currentLocation) + { + var pathCommonPath = PathHelper.GetCommonPath(currentLocation.FullName!, container.FullName!); + RealtiveParentPath = new AbsolutePath(null!, container.FullName!.Substring(pathCommonPath.Length).Trim(Constants.SeparatorChar), AbsolutePathType.Unknown, null).GetParentPath(); + ItemNameParts = await Task.Run(async () => await Dispatcher.UIThread.InvokeAsync(() => container.SearchDisplayName.Select(p => new ItemNamePartViewModel(p.Text, p.IsSpecial ? TextDecorations.Underline : null)).ToList())); + } + } +} \ 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 new file mode 100644 index 0000000..ba6a714 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ItemPreview/SearchElementPreview.cs @@ -0,0 +1,35 @@ +using Avalonia.Media; +using Avalonia.Threading; +using FileTime.Avalonia.Models; +using FileTime.Avalonia.Services; +using FileTime.Core.Helper; +using FileTime.Core.Models; +using FileTime.Core.Search; +using MvvmGen; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace FileTime.Avalonia.ViewModels.ItemPreview +{ + [ViewModel] + public partial class SearchElementPreview : IItemPreviewViewModel + { + + [Property] + private ItemPreviewMode _mode = ItemPreviewMode.SearchElement; + + [Property] + private List? _itemNameParts; + + [Property] + private string? _realtiveParentPath; + + public async Task Init(ChildSearchElement element, IContainer currentLocation) + { + var pathCommonPath = PathHelper.GetCommonPath(currentLocation.FullName!, element.FullName!); + RealtiveParentPath = new AbsolutePath(null!, element.FullName!.Substring(pathCommonPath.Length).Trim(Constants.SeparatorChar), AbsolutePathType.Unknown, null).GetParentPath(); + ItemNameParts = await Task.Run(async () => await Dispatcher.UIThread.InvokeAsync(() => element.SearchDisplayName.Select(p => new ItemNamePartViewModel(p.Text, p.IsSpecial ? TextDecorations.Underline : null)).ToList())); + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs index 3d021b0..bd718da 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs @@ -17,11 +17,11 @@ using Microsoft.Extensions.DependencyInjection; using FileTime.Core.Timeline; using FileTime.Core.Providers; using Syroot.Windows.IO; -using FileTime.Avalonia.IconProviders; using Microsoft.Extensions.Logging; using System.Threading; using Avalonia.Input; using System.Reflection; +using FileTime.Core.Services; namespace FileTime.Avalonia.ViewModels { @@ -35,6 +35,7 @@ namespace FileTime.Avalonia.ViewModels [Inject(typeof(CommandHandlerService), PropertyAccessModifier = AccessModifier.Public)] [Inject(typeof(IDialogService), PropertyName = "_dialogService")] [Inject(typeof(KeyInputHandlerService))] + [Inject(typeof(IServiceProvider), PropertyName = "_serviceProvider")] public partial class MainPageViewModel : IMainPageViewModelBase { public const string RAPIDTRAVEL = "rapidTravel"; @@ -84,7 +85,7 @@ namespace FileTime.Avalonia.ViewModels var tab = new Tab(); await tab.Init(LocalContentProvider); - var tabContainer = new TabContainer(_timeRunner, tab, LocalContentProvider, ItemNameConverterService); + var tabContainer = new TabContainer(_serviceProvider, _timeRunner, tab, LocalContentProvider, ItemNameConverterService, AppState); await tabContainer.Init(1); tabContainer.IsSelected = true; AppState.Tabs.Add(tabContainer); diff --git a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml index 8d4f928..7d3dd6b 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml +++ b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml @@ -37,7 +37,7 @@ + Text="{Binding AppState.SelectedTab.SelectedItem.Item.DisplayName}" Foreground="{StaticResource AccentBrush}" /> @@ -242,8 +242,12 @@ VerticalAlignment="Stretch" Fill="{DynamicResource ContentSeparatorBrush}" /> - + + + + - - Empty - + + + Empty + + - - - - + + + + + x:CompileBindings="False" + Text="{Binding AppState.SelectedTab.ItemPreview.TextContent}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -494,10 +552,10 @@ KeyDown="InputText_KeyDown" VerticalAlignment="Top" Text="{Binding Value, Mode=TwoWay}" /> - diff --git a/src/Providers/FileTime.Providers.Local/LocalFile.cs b/src/Providers/FileTime.Providers.Local/LocalFile.cs index 7dd0ade..51ce643 100644 --- a/src/Providers/FileTime.Providers.Local/LocalFile.cs +++ b/src/Providers/FileTime.Providers.Local/LocalFile.cs @@ -12,6 +12,7 @@ namespace FileTime.Providers.Local public FileInfo File { get; } public string Name { get; } + public string DisplayName { get; } public string FullName { get; } public string? NativePath => File.FullName; @@ -39,7 +40,7 @@ namespace FileTime.Providers.Local _parent = parent; File = file; - Name = file.Name; + DisplayName = Name = file.Name; FullName = parent.FullName + Constants.SeparatorChar + file.Name; Provider = contentProvider; } @@ -86,7 +87,7 @@ namespace FileTime.Providers.Local public IContainer? GetParent() => _parent; public async Task GetContent(CancellationToken token = default) => await System.IO.File.ReadAllTextAsync(File.FullName, token); - public Task GetElementSize(CancellationToken token = default) => Task.FromResult(File.Length); + public Task GetElementSize(CancellationToken token = default) => Task.FromResult((long?)File.Length); public void Destroy() => IsDestroyed = true; diff --git a/src/Providers/FileTime.Providers.Sftp/SftpFile.cs b/src/Providers/FileTime.Providers.Sftp/SftpFile.cs index d30feed..39048b9 100644 --- a/src/Providers/FileTime.Providers.Sftp/SftpFile.cs +++ b/src/Providers/FileTime.Providers.Sftp/SftpFile.cs @@ -8,6 +8,7 @@ namespace FileTime.Providers.Sftp public bool IsSpecial => throw new NotImplementedException(); public string Name => throw new NotImplementedException(); + public string DisplayName => throw new NotImplementedException(); public string? FullName => throw new NotImplementedException(); @@ -50,7 +51,7 @@ namespace FileTime.Providers.Sftp throw new NotImplementedException(); } - public Task GetElementSize(CancellationToken token = default) + public Task GetElementSize(CancellationToken token = default) { throw new NotImplementedException(); } diff --git a/src/Providers/FileTime.Providers.Smb/SmbFile.cs b/src/Providers/FileTime.Providers.Smb/SmbFile.cs index 48638a8..4717998 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbFile.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbFile.cs @@ -13,6 +13,7 @@ namespace FileTime.Providers.Smb public bool IsSpecial => false; public string Name { get; } + public string DisplayName { get; } public string? FullName { get; } public string? NativePath { get; } @@ -34,7 +35,7 @@ namespace FileTime.Providers.Smb _smbClientContext = smbClientContext; _smbShare = smbShare; - Name = name; + DisplayName = Name = name; FullName = parent.FullName + Constants.SeparatorChar + Name; NativePath = parent.NativePath + SmbContentProvider.GetNativePathSeparator() + name; } @@ -80,7 +81,7 @@ namespace FileTime.Providers.Smb public IContainer? GetParent() => _parent; public Task GetContent(CancellationToken token = default) => Task.FromResult("NotImplemented"); - public Task GetElementSize(CancellationToken token = default) => Task.FromResult(-1L); + public Task GetElementSize(CancellationToken token = default) => Task.FromResult((long?)null); public void Destroy() => IsDestroyed = true;