Frequency navigation WIP
This commit is contained in:
@@ -9,4 +9,5 @@ public interface IModalService
|
||||
|
||||
void OpenModal(IModalViewModel modalToOpen);
|
||||
void CloseModal(IModalViewModel modalToClose);
|
||||
T OpenModal<T>() where T : IModalViewModel;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace FileTime.App.Core.UserCommand;
|
||||
|
||||
public class GoByFrequencyCommand : IIdentifiableUserCommand
|
||||
{
|
||||
public const string CommandName = "go_by_frequency";
|
||||
|
||||
public static GoByFrequencyCommand Instance { get; } = new();
|
||||
|
||||
private GoByFrequencyCommand()
|
||||
{
|
||||
}
|
||||
|
||||
public string UserCommandID => CommandName;
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
<ProjectReference Include="..\..\Tools\FileTime.Tools\FileTime.Tools.csproj" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -63,7 +63,7 @@ public class TabPersistenceService : ITabPersistenceService
|
||||
_serviceProvider = serviceProvider;
|
||||
_localContentProvider = localContentProvider;
|
||||
|
||||
_jsonOptions = new JsonSerializerOptions()
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true
|
||||
|
||||
@@ -2,6 +2,7 @@ using FileTime.App.Core.Extensions;
|
||||
using FileTime.App.Core.Models.Enums;
|
||||
using FileTime.App.Core.UserCommand;
|
||||
using FileTime.App.Core.ViewModels;
|
||||
using FileTime.App.FrequencyNavigation.Services;
|
||||
using FileTime.Core.Interactions;
|
||||
using FileTime.Core.Models;
|
||||
using FileTime.Core.Services;
|
||||
@@ -20,6 +21,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
||||
private readonly IUserCommandHandlerService _userCommandHandlerService;
|
||||
private readonly ITimelessContentProvider _timelessContentProvider;
|
||||
private readonly IUserCommunicationService _userCommunicationService;
|
||||
private readonly IFrequencyNavigationService _frequencyNavigationService;
|
||||
private ITabViewModel? _selectedTab;
|
||||
private IContainer? _currentLocation;
|
||||
private IItemViewModel? _currentSelectedItem;
|
||||
@@ -32,7 +34,8 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
||||
ILocalContentProvider localContentProvider,
|
||||
IUserCommandHandlerService userCommandHandlerService,
|
||||
ITimelessContentProvider timelessContentProvider,
|
||||
IUserCommunicationService userCommunicationService) : base(appState)
|
||||
IUserCommunicationService userCommunicationService,
|
||||
IFrequencyNavigationService frequencyNavigationService) : base(appState)
|
||||
{
|
||||
_appState = appState;
|
||||
_serviceProvider = serviceProvider;
|
||||
@@ -40,6 +43,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
||||
_userCommandHandlerService = userCommandHandlerService;
|
||||
_timelessContentProvider = timelessContentProvider;
|
||||
_userCommunicationService = userCommunicationService;
|
||||
_frequencyNavigationService = frequencyNavigationService;
|
||||
|
||||
SaveSelectedTab(t => _selectedTab = t);
|
||||
SaveCurrentSelectedItem(i => _currentSelectedItem = i);
|
||||
@@ -53,6 +57,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
||||
new TypeUserCommandHandler<CloseTabCommand>(CloseTab),
|
||||
new TypeUserCommandHandler<EnterRapidTravelCommand>(EnterRapidTravel),
|
||||
new TypeUserCommandHandler<ExitRapidTravelCommand>(ExitRapidTravel),
|
||||
new TypeUserCommandHandler<GoByFrequencyCommand>(GoByFrequency),
|
||||
new TypeUserCommandHandler<GoToHomeCommand>(GoToHome),
|
||||
new TypeUserCommandHandler<GoToPathCommand>(GoToPath),
|
||||
new TypeUserCommandHandler<GoToProviderCommand>(GoToProvider),
|
||||
@@ -71,6 +76,12 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
|
||||
});
|
||||
}
|
||||
|
||||
private Task GoByFrequency()
|
||||
{
|
||||
_frequencyNavigationService.OpenNavigationWindow();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task GoToPath()
|
||||
{
|
||||
var pathInput = new TextInputElement("Path");
|
||||
|
||||
@@ -20,6 +20,7 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler
|
||||
AddUserCommand(DeleteCommand.SoftDelete);
|
||||
AddUserCommand(EnterRapidTravelCommand.Instance);
|
||||
AddUserCommand(ExitRapidTravelCommand.Instance);
|
||||
AddUserCommand(GoByFrequencyCommand.Instance);
|
||||
AddUserCommand(GoToHomeCommand.Instance);
|
||||
AddUserCommand(GoToPathCommand.Instance);
|
||||
AddUserCommand(GoToProviderCommand.Instance);
|
||||
|
||||
@@ -35,6 +35,7 @@ public static class DependencyInjection
|
||||
serviceCollection.TryAddSingleton<IApplicationSettings, ApplicationSettings>();
|
||||
serviceCollection.TryAddSingleton<ITabPersistenceService, TabPersistenceService>();
|
||||
serviceCollection.TryAddTransient<ITab, Tab>();
|
||||
serviceCollection.TryAddSingleton<ITabEvents, TabEvents>();
|
||||
serviceCollection.AddSingleton<IExitHandler, ITabPersistenceService>(sp => sp.GetRequiredService<ITabPersistenceService>());
|
||||
serviceCollection.AddSingleton<IStartupHandler, ITabPersistenceService>(sp => sp.GetRequiredService<ITabPersistenceService>());
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>FileTime.App.FrequencyNavigation</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
using FileTime.App.FrequencyNavigation.ViewModels;
|
||||
|
||||
namespace FileTime.App.FrequencyNavigation.Services;
|
||||
|
||||
public interface IFrequencyNavigationService
|
||||
{
|
||||
IObservable<bool> ShowWindow { get; }
|
||||
IFrequencyNavigationViewModel? CurrentModal { get; }
|
||||
void OpenNavigationWindow();
|
||||
void CloseNavigationWindow();
|
||||
IList<string> GetMatchingContainers(string searchText);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using FileTime.App.Core.ViewModels;
|
||||
|
||||
namespace FileTime.App.FrequencyNavigation.ViewModels;
|
||||
|
||||
public interface IFrequencyNavigationViewModel : IModalViewModel
|
||||
{
|
||||
IObservable<bool> ShowWindow { get; }
|
||||
List<string> FilteredMatches { get; }
|
||||
string SearchText { get; set; }
|
||||
string SelectedItem { get; set; }
|
||||
void Close();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<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="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||
<ProjectReference Include="..\FileTime.App.FrequencyNavigation.Abstractions\FileTime.App.FrequencyNavigation.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="MvvmGen" Version="1.1.5" />
|
||||
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FileTime.App.FrequencyNavigation.Models;
|
||||
|
||||
public class ContainerFrequencyData
|
||||
{
|
||||
public int Score { get; set; } = 1;
|
||||
public DateTime LastAccessed { get; set; } = DateTime.Now;
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Text.Json;
|
||||
using FileTime.App.Core.Models;
|
||||
using FileTime.App.Core.Services;
|
||||
using FileTime.App.FrequencyNavigation.Models;
|
||||
using FileTime.App.FrequencyNavigation.ViewModels;
|
||||
using FileTime.Core.Models;
|
||||
using FileTime.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
|
||||
namespace FileTime.App.FrequencyNavigation.Services;
|
||||
|
||||
public partial class FrequencyNavigationService : IFrequencyNavigationService, IStartupHandler, IExitHandler
|
||||
{
|
||||
private const int MaxAge = 10_000;
|
||||
|
||||
private DateTime _lastSave = DateTime.Now;
|
||||
private readonly ILogger<FrequencyNavigationService> _logger;
|
||||
private readonly IModalService _modalService;
|
||||
private readonly SemaphoreSlim _saveLock = new(1);
|
||||
private Dictionary<string, ContainerFrequencyData> _containerScores = new();
|
||||
private readonly BehaviorSubject<bool> _showWindow = new(false);
|
||||
private readonly string _dbPath;
|
||||
[Notify] IFrequencyNavigationViewModel? _currentModal;
|
||||
IObservable<bool> IFrequencyNavigationService.ShowWindow => _showWindow.AsObservable();
|
||||
|
||||
public FrequencyNavigationService(
|
||||
ITabEvents tabEvents,
|
||||
IApplicationSettings applicationSettings,
|
||||
ILogger<FrequencyNavigationService> logger,
|
||||
IModalService modalService)
|
||||
{
|
||||
_logger = logger;
|
||||
_modalService = modalService;
|
||||
_dbPath = Path.Combine(applicationSettings.AppDataRoot, "frequencyNavigationScores.json");
|
||||
tabEvents.LocationChanged += OnTabLocationChanged;
|
||||
}
|
||||
|
||||
void OnTabLocationChanged(object? sender, TabLocationChanged e)
|
||||
{
|
||||
IncreaseContainerScore(e.Location);
|
||||
}
|
||||
|
||||
public void OpenNavigationWindow()
|
||||
{
|
||||
_showWindow.OnNext(true);
|
||||
CurrentModal = _modalService.OpenModal<IFrequencyNavigationViewModel>();
|
||||
}
|
||||
|
||||
public void CloseNavigationWindow()
|
||||
{
|
||||
_showWindow.OnNext(false);
|
||||
if (_currentModal is not null)
|
||||
{
|
||||
_modalService.CloseModal(_currentModal);
|
||||
CurrentModal = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void IncreaseContainerScore(FullName containerName)
|
||||
{
|
||||
await _saveLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
var containerNameString = containerName.Path;
|
||||
if (_containerScores.ContainsKey(containerNameString))
|
||||
{
|
||||
_containerScores[containerNameString].Score++;
|
||||
_containerScores[containerNameString].LastAccessed = DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
_containerScores.Add(containerNameString, new ContainerFrequencyData());
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error increasing container score");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_saveLock.Release();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (TryAgeContainerScores() || DateTime.Now - _lastSave > TimeSpan.FromMinutes(5))
|
||||
{
|
||||
}
|
||||
|
||||
await SaveStateAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error aging container scores");
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryAgeContainerScores()
|
||||
{
|
||||
if (_containerScores.Select(c => c.Value.Score).Sum() < MaxAge)
|
||||
return false;
|
||||
|
||||
AgeContainerScores();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AgeContainerScores()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var itemsToRemove = new List<string>();
|
||||
foreach (var container in _containerScores)
|
||||
{
|
||||
var newScore = (int) Math.Floor(container.Value.Score * 0.9);
|
||||
if (newScore > 0)
|
||||
{
|
||||
container.Value.Score = newScore;
|
||||
}
|
||||
else
|
||||
{
|
||||
itemsToRemove.Add(container.Key);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var itemToRemove in itemsToRemove)
|
||||
{
|
||||
_containerScores.Remove(itemToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
public IList<string> GetMatchingContainers(string searchText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchText))
|
||||
return new List<string>();
|
||||
|
||||
_saveLock.Wait();
|
||||
var matchingContainers = _containerScores
|
||||
.Where(c => c.Key.Contains(searchText, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(c => GetWeightedScore(c.Value.Score, c.Value.LastAccessed))
|
||||
.Select(c => c.Key)
|
||||
.ToList();
|
||||
|
||||
_saveLock.Release();
|
||||
return matchingContainers;
|
||||
}
|
||||
|
||||
private int GetWeightedScore(int score, DateTime lastAccess)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var timeSinceLastAccess = now - lastAccess;
|
||||
return timeSinceLastAccess.TotalHours switch
|
||||
{
|
||||
< 1 => score *= 4,
|
||||
< 24 => score *= 2,
|
||||
< 168 => score /= 2,
|
||||
_ => score /= 4
|
||||
};
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await LoadStateAsync();
|
||||
}
|
||||
|
||||
private async Task LoadStateAsync()
|
||||
{
|
||||
if (!File.Exists(_dbPath))
|
||||
return;
|
||||
|
||||
await _saveLock.WaitAsync();
|
||||
await using var dbStream = File.OpenRead(_dbPath);
|
||||
var containerScores = await JsonSerializer.DeserializeAsync<Dictionary<string, ContainerFrequencyData>>(dbStream);
|
||||
if (containerScores is null) return;
|
||||
|
||||
_containerScores = containerScores;
|
||||
_saveLock.Release();
|
||||
}
|
||||
|
||||
public async Task ExitAsync()
|
||||
{
|
||||
await SaveStateAsync();
|
||||
}
|
||||
|
||||
private async Task SaveStateAsync()
|
||||
{
|
||||
await _saveLock.WaitAsync();
|
||||
_lastSave = DateTime.Now;
|
||||
await using var dbStream = File.OpenWrite(_dbPath);
|
||||
await JsonSerializer.SerializeAsync(dbStream, _containerScores);
|
||||
_saveLock.Release();
|
||||
}
|
||||
}
|
||||
20
src/AppCommon/FileTime.App.FrequencyNavigation/Startup.cs
Normal file
20
src/AppCommon/FileTime.App.FrequencyNavigation/Startup.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using FileTime.App.Core.Services;
|
||||
using FileTime.App.FrequencyNavigation.Services;
|
||||
using FileTime.App.FrequencyNavigation.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace FileTime.App.FrequencyNavigation;
|
||||
|
||||
public static class Startup
|
||||
{
|
||||
public static IServiceCollection AddFrequencyNavigation(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddTransient<IFrequencyNavigationViewModel, FrequencyNavigationViewModel>();
|
||||
services.AddSingleton<FrequencyNavigationService>();
|
||||
services.TryAddSingleton<IFrequencyNavigationService>(sp => sp.GetRequiredService<FrequencyNavigationService>());
|
||||
services.AddSingleton<IStartupHandler>(sp => sp.GetRequiredService<FrequencyNavigationService>());
|
||||
services.AddSingleton<IExitHandler>(sp => sp.GetRequiredService<FrequencyNavigationService>());
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using FileTime.App.Core.ViewModels;
|
||||
using FileTime.App.FrequencyNavigation.Services;
|
||||
using MvvmGen;
|
||||
|
||||
namespace FileTime.App.FrequencyNavigation.ViewModels;
|
||||
|
||||
[ViewModel]
|
||||
[Inject(typeof(IFrequencyNavigationService), "_frequencyNavigationService")]
|
||||
public partial class FrequencyNavigationViewModel : IFrequencyNavigationViewModel
|
||||
{
|
||||
private string _searchText;
|
||||
|
||||
[Property] private IObservable<bool> _showWindow;
|
||||
[Property] private List<string> _filteredMatches;
|
||||
[Property] private string _selectedItem;
|
||||
|
||||
public string SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set
|
||||
{
|
||||
if (_searchText == value) return;
|
||||
|
||||
_searchText = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
UpdateFilteredMatches();
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_frequencyNavigationService.CloseNavigationWindow();
|
||||
}
|
||||
|
||||
partial void OnInitialize()
|
||||
{
|
||||
_showWindow = _frequencyNavigationService.ShowWindow;
|
||||
}
|
||||
|
||||
private void UpdateFilteredMatches()
|
||||
{
|
||||
FilteredMatches = new List<string>(_frequencyNavigationService.GetMatchingContainers(_searchText));
|
||||
}
|
||||
|
||||
string IModalViewModel.Name => "FrequencyNavigation";
|
||||
}
|
||||
Reference in New Issue
Block a user