Search WIP
This commit is contained in:
@@ -8,9 +8,9 @@ public sealed class PasteCommand : IIdentifiableUserCommand
|
||||
public const string PasteOverwriteCommandName = "paste_overwrite";
|
||||
public const string PasteSkipCommandName = "paste_skip";
|
||||
|
||||
public static PasteCommand Merge { get; } = new PasteCommand(PasteMode.Merge, PasteMergeCommandName);
|
||||
public static PasteCommand Overwrite { get; } = new PasteCommand(PasteMode.Overwrite, PasteOverwriteCommandName);
|
||||
public static PasteCommand Skip { get; } = new PasteCommand(PasteMode.Skip, PasteSkipCommandName);
|
||||
public static readonly PasteCommand Merge = new(PasteMode.Merge, PasteMergeCommandName);
|
||||
public static readonly PasteCommand Overwrite = new(PasteMode.Overwrite, PasteOverwriteCommandName);
|
||||
public static readonly PasteCommand Skip = new(PasteMode.Skip, PasteSkipCommandName);
|
||||
|
||||
public PasteMode PasteMode { get; }
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace FileTime.App.Core.UserCommand;
|
||||
|
||||
public enum SearchType
|
||||
{
|
||||
NameContains,
|
||||
NameRegex
|
||||
}
|
||||
|
||||
public class SearchCommand : IUserCommand
|
||||
{
|
||||
public string? SearchText { get; }
|
||||
public SearchType SearchType { get; }
|
||||
|
||||
public SearchCommand(string? searchText, SearchType searchType)
|
||||
{
|
||||
SearchText = searchText;
|
||||
SearchType = searchType;
|
||||
}
|
||||
}
|
||||
|
||||
public class IdentifiableSearchCommand : SearchCommand, IIdentifiableUserCommand
|
||||
{
|
||||
public const string SearchByNameContainsCommandName = "search_name_contains";
|
||||
|
||||
public static readonly IdentifiableSearchCommand SearchByNameContains =
|
||||
new(null, SearchType.NameContains, SearchByNameContainsCommandName);
|
||||
|
||||
public IdentifiableSearchCommand(
|
||||
string? searchText,
|
||||
SearchType searchType,
|
||||
string commandId)
|
||||
: base(searchText, searchType)
|
||||
{
|
||||
UserCommandID = commandId;
|
||||
}
|
||||
|
||||
public string UserCommandID { get; }
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||
<ProjectReference Include="..\..\Core\FileTime.Core.Command\FileTime.Core.Command.csproj" />
|
||||
<ProjectReference Include="..\FileTime.App.FrequencyNavigation.Abstractions\FileTime.App.FrequencyNavigation.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\FileTime.App.Search\FileTime.App.Search.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
using FileTime.App.Core.UserCommand;
|
||||
using FileTime.App.Core.ViewModels;
|
||||
using FileTime.App.Search;
|
||||
using FileTime.Core.Interactions;
|
||||
using FileTime.Core.Models;
|
||||
|
||||
namespace FileTime.App.Core.Services.UserCommandHandler;
|
||||
@@ -8,12 +10,23 @@ namespace FileTime.App.Core.Services.UserCommandHandler;
|
||||
public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
|
||||
{
|
||||
private readonly ISystemClipboardService _systemClipboardService;
|
||||
private readonly IUserCommunicationService _userCommunicationService;
|
||||
private readonly ISearchManager _searchManager;
|
||||
private readonly IItemNameConverterService _itemNameConverterService;
|
||||
private IContainer? _currentLocation;
|
||||
private IItemViewModel? _currentSelectedItem;
|
||||
|
||||
public ToolUserCommandHandlerService(IAppState appState, ISystemClipboardService systemClipboardService) : base(appState)
|
||||
public ToolUserCommandHandlerService(
|
||||
IAppState appState,
|
||||
ISystemClipboardService systemClipboardService,
|
||||
IUserCommunicationService userCommunicationService,
|
||||
ISearchManager searchManager,
|
||||
IItemNameConverterService itemNameConverterService) : base(appState)
|
||||
{
|
||||
_systemClipboardService = systemClipboardService;
|
||||
_userCommunicationService = userCommunicationService;
|
||||
_searchManager = searchManager;
|
||||
_itemNameConverterService = itemNameConverterService;
|
||||
SaveCurrentLocation(l => _currentLocation = l);
|
||||
SaveCurrentSelectedItem(i => _currentSelectedItem = i);
|
||||
|
||||
@@ -21,9 +34,45 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
|
||||
{
|
||||
new TypeUserCommandHandler<OpenInDefaultFileExplorerCommand>(OpenInDefaultFileExplorer),
|
||||
new TypeUserCommandHandler<CopyNativePathCommand>(CopyNativePath),
|
||||
new TypeUserCommandHandler<SearchCommand>(Search),
|
||||
});
|
||||
}
|
||||
|
||||
private async Task Search(SearchCommand searchCommand)
|
||||
{
|
||||
if(_currentLocation is null) return;
|
||||
|
||||
var searchQuery = searchCommand.SearchText;
|
||||
if (string.IsNullOrEmpty(searchQuery))
|
||||
{
|
||||
var title = searchCommand.SearchType switch
|
||||
{
|
||||
SearchType.NameContains => "Search by Name",
|
||||
SearchType.NameRegex => "Search by Name (Regex)",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
var containerNameInput = new TextInputElement(title);
|
||||
await _userCommunicationService.ReadInputs(containerNameInput);
|
||||
|
||||
if (containerNameInput.Value is not null)
|
||||
{
|
||||
searchQuery = containerNameInput.Value;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO proper error message
|
||||
if(string.IsNullOrWhiteSpace(searchQuery)) return;
|
||||
|
||||
var searchMatcher = searchCommand.SearchType switch
|
||||
{
|
||||
SearchType.NameContains => new NameContainsMatcher(_itemNameConverterService, searchQuery),
|
||||
//SearchType.NameRegex => new NameRegexMatcher(searchQuery),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
await _searchManager.StartSearchAsync(searchMatcher, _currentLocation);
|
||||
}
|
||||
|
||||
private async Task CopyNativePath()
|
||||
{
|
||||
if (_currentSelectedItem?.BaseItem?.NativePath is null) return;
|
||||
|
||||
@@ -2,7 +2,7 @@ using FileTime.App.Core.UserCommand;
|
||||
|
||||
namespace FileTime.App.Core.Services.UserCommandHandler;
|
||||
|
||||
public class TypeUserCommandHandler<T> : IUserCommandHandler
|
||||
public class TypeUserCommandHandler<T> : IUserCommandHandler where T : IUserCommand
|
||||
{
|
||||
private readonly Func<T, Task> _handler;
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler
|
||||
AddUserCommand(PauseCommandSchedulerCommand.Instance);
|
||||
AddUserCommand(RefreshCommand.Instance);
|
||||
AddUserCommand(StartCommandSchedulerCommand.Instance);
|
||||
AddUserCommand(IdentifiableSearchCommand.SearchByNameContains);
|
||||
AddUserCommand(SwitchToTabCommand.SwitchToLastTab);
|
||||
AddUserCommand(SwitchToTabCommand.SwitchToTab1);
|
||||
AddUserCommand(SwitchToTabCommand.SwitchToTab2);
|
||||
|
||||
@@ -19,7 +19,7 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I
|
||||
private DateTime _lastSave = DateTime.Now;
|
||||
private readonly ILogger<FrequencyNavigationService> _logger;
|
||||
private readonly IModalService _modalService;
|
||||
private readonly SemaphoreSlim _saveLock = new(1);
|
||||
private readonly SemaphoreSlim _saveLock = new(1, 1);
|
||||
private Dictionary<string, ContainerFrequencyData> _containerScores = new();
|
||||
private readonly BehaviorSubject<bool> _showWindow = new(false);
|
||||
private readonly string _dbPath;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>FileTime.App.Search</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
|
||||
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
using FileTime.Core.ContentAccess;
|
||||
|
||||
namespace FileTime.App.Search;
|
||||
|
||||
public interface ISearchContentProvider : IContentProvider
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using FileTime.Core.Models;
|
||||
|
||||
namespace FileTime.App.Search;
|
||||
|
||||
public interface ISearchManager
|
||||
{
|
||||
Task StartSearchAsync(ISearchMatcher matcher, IContainer searchIn);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using FileTime.App.Core.Models;
|
||||
using FileTime.Core.Models;
|
||||
|
||||
namespace FileTime.App.Search;
|
||||
|
||||
public interface ISearchMatcher
|
||||
{
|
||||
Task<bool> IsItemMatchAsync(IItem item);
|
||||
List<ItemNamePart> GetDisplayName(IItem item);
|
||||
}
|
||||
20
src/AppCommon/FileTime.App.Search/FileTime.App.Search.csproj
Normal file
20
src/AppCommon/FileTime.App.Search/FileTime.App.Search.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
|
||||
<ProjectReference Include="..\..\Core\FileTime.Core.Models\FileTime.Core.Models.csproj" />
|
||||
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||
<ProjectReference Include="..\FileTime.App.Search.Abstractions\FileTime.App.Search.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
21
src/AppCommon/FileTime.App.Search/NameContainsMatcher.cs
Normal file
21
src/AppCommon/FileTime.App.Search/NameContainsMatcher.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using FileTime.App.Core.Models;
|
||||
using FileTime.App.Core.Services;
|
||||
using FileTime.Core.Models;
|
||||
|
||||
namespace FileTime.App.Search;
|
||||
|
||||
public class NameContainsMatcher : ISearchMatcher
|
||||
{
|
||||
private readonly IItemNameConverterService _itemNameConverterService;
|
||||
private readonly string _searchText;
|
||||
|
||||
public NameContainsMatcher(IItemNameConverterService itemNameConverterService, string searchText)
|
||||
{
|
||||
_itemNameConverterService = itemNameConverterService;
|
||||
_searchText = searchText;
|
||||
}
|
||||
|
||||
public Task<bool> IsItemMatchAsync(IItem item) => Task.FromResult(item.Name.Contains(_searchText, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public List<ItemNamePart> GetDisplayName(IItem item) => _itemNameConverterService.GetDisplayName(item.DisplayName, _searchText);
|
||||
}
|
||||
44
src/AppCommon/FileTime.App.Search/SearchContentProvider.cs
Normal file
44
src/AppCommon/FileTime.App.Search/SearchContentProvider.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using DynamicData;
|
||||
using FileTime.Core.ContentAccess;
|
||||
using FileTime.Core.Enums;
|
||||
using FileTime.Core.Models;
|
||||
using FileTime.Core.Timeline;
|
||||
|
||||
namespace FileTime.App.Search;
|
||||
|
||||
public class SearchContentProvider : ISearchContentProvider
|
||||
{
|
||||
public string Name { get; }
|
||||
public string DisplayName { get; }
|
||||
public FullName? FullName { get; }
|
||||
public NativePath? NativePath { get; }
|
||||
public AbsolutePath? Parent { get; }
|
||||
public bool IsHidden { get; }
|
||||
public bool IsExists { get; }
|
||||
public DateTime? CreatedAt { get; }
|
||||
public SupportsDelete CanDelete { get; }
|
||||
public bool CanRename { get; }
|
||||
public IContentProvider Provider { get; }
|
||||
public string? Attributes { get; }
|
||||
public AbsolutePathType Type { get; }
|
||||
public PointInTime PointInTime { get; }
|
||||
public IObservable<IChangeSet<Exception>> Exceptions { get; }
|
||||
public ReadOnlyExtensionCollection Extensions { get; }
|
||||
public IObservable<IObservable<IChangeSet<AbsolutePath, string>>?> Items { get; }
|
||||
public IObservable<bool> IsLoading { get; }
|
||||
public bool AllowRecursiveDeletion { get; }
|
||||
public Task OnEnter() => throw new NotImplementedException();
|
||||
|
||||
public bool SupportsContentStreams { get; }
|
||||
public Task<IItem> GetItemByFullNameAsync(FullName fullName, PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, ItemInitializationSettings itemInitializationSettings = default) => throw new NotImplementedException();
|
||||
|
||||
public Task<IItem> GetItemByNativePathAsync(NativePath nativePath, PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, ItemInitializationSettings itemInitializationSettings = default) => throw new NotImplementedException();
|
||||
|
||||
public NativePath GetNativePath(FullName fullName) => throw new NotImplementedException();
|
||||
|
||||
public Task<byte[]?> GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
public bool CanHandlePath(NativePath path) => throw new NotImplementedException();
|
||||
|
||||
public bool CanHandlePath(FullName path) => throw new NotImplementedException();
|
||||
}
|
||||
27
src/AppCommon/FileTime.App.Search/SearchManager.cs
Normal file
27
src/AppCommon/FileTime.App.Search/SearchManager.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using FileTime.Core.Models;
|
||||
|
||||
namespace FileTime.App.Search;
|
||||
|
||||
public class SearchManager : ISearchManager
|
||||
{
|
||||
private readonly ISearchContentProvider _searchContainerProvider;
|
||||
private readonly List<SearchTask> _searchTasks = new();
|
||||
|
||||
public SearchManager(ISearchContentProvider searchContainerProvider)
|
||||
{
|
||||
_searchContainerProvider = searchContainerProvider;
|
||||
}
|
||||
|
||||
public async Task StartSearchAsync(ISearchMatcher matcher, IContainer searchIn)
|
||||
{
|
||||
var searchTask = new SearchTask(
|
||||
searchIn,
|
||||
_searchContainerProvider,
|
||||
matcher
|
||||
);
|
||||
|
||||
_searchTasks.Add(searchTask);
|
||||
|
||||
await searchTask.StartAsync();
|
||||
}
|
||||
}
|
||||
101
src/AppCommon/FileTime.App.Search/SearchTask.cs
Normal file
101
src/AppCommon/FileTime.App.Search/SearchTask.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.Reactive.Linq;
|
||||
using DynamicData;
|
||||
using FileTime.Core.ContentAccess;
|
||||
using FileTime.Core.Enums;
|
||||
using FileTime.Core.Extensions;
|
||||
using FileTime.Core.Models;
|
||||
using FileTime.Core.Timeline;
|
||||
|
||||
namespace FileTime.App.Search;
|
||||
|
||||
public class SearchTask
|
||||
{
|
||||
private readonly IContainer _baseContainer;
|
||||
private readonly ISearchMatcher _matcher;
|
||||
private readonly Container _container;
|
||||
private readonly SourceList<Exception> _exceptions = new();
|
||||
private readonly SourceCache<AbsolutePath, string> _items = new(p => p.Path.Path);
|
||||
private readonly SemaphoreSlim _searchingLock = new(1, 1);
|
||||
private bool _isSearching;
|
||||
|
||||
public SearchTask(
|
||||
IContainer baseContainer,
|
||||
IContentProvider contentProvider,
|
||||
ISearchMatcher matcher
|
||||
)
|
||||
{
|
||||
_baseContainer = baseContainer;
|
||||
_matcher = matcher;
|
||||
_container = new Container(
|
||||
baseContainer.Name,
|
||||
baseContainer.DisplayName,
|
||||
new FullName(""),
|
||||
new NativePath(""),
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
SupportsDelete.False,
|
||||
false,
|
||||
null,
|
||||
contentProvider,
|
||||
false,
|
||||
PointInTime.Present,
|
||||
_exceptions.Connect(),
|
||||
new ReadOnlyExtensionCollection(new ExtensionCollection()),
|
||||
Observable.Return(_items.Connect())
|
||||
);
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
await _searchingLock.WaitAsync();
|
||||
if (_isSearching) return;
|
||||
_isSearching = true;
|
||||
_searchingLock.Release();
|
||||
|
||||
Task.Run(BootstrapSearch);
|
||||
|
||||
async Task BootstrapSearch()
|
||||
{
|
||||
try
|
||||
{
|
||||
_container.IsLoading.OnNext(true);
|
||||
await TraverseTree(_baseContainer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_container.IsLoading.OnNext(false);
|
||||
|
||||
await _searchingLock.WaitAsync();
|
||||
_isSearching = false;
|
||||
_searchingLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TraverseTree(IContainer container)
|
||||
{
|
||||
var items = (await container.Items.GetItemsAsync())?.ToList();
|
||||
if (items is null) return;
|
||||
|
||||
var childContainers = new List<IContainer>();
|
||||
|
||||
foreach (var itemPath in items)
|
||||
{
|
||||
var item = await itemPath.ResolveAsync();
|
||||
if (await _matcher.IsItemMatchAsync(item))
|
||||
{
|
||||
_items.AddOrUpdate(itemPath);
|
||||
}
|
||||
|
||||
if (item is IContainer childContainer)
|
||||
childContainers.Add(childContainer);
|
||||
}
|
||||
|
||||
foreach (var childContainer in childContainers)
|
||||
{
|
||||
await TraverseTree(childContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/AppCommon/FileTime.App.Search/Startup.cs
Normal file
14
src/AppCommon/FileTime.App.Search/Startup.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FileTime.App.Search;
|
||||
|
||||
public static class Startup
|
||||
{
|
||||
public static IServiceCollection AddSearch(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ISearchContentProvider, SearchContentProvider>();
|
||||
services.AddSingleton<ISearchManager, SearchManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user