Frequency navigation WIP

This commit is contained in:
2023-02-24 22:05:13 +01:00
parent 188b9593ce
commit 3d057f947a
34 changed files with 576 additions and 42 deletions

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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();
}
}

View 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;
}
}

View File

@@ -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";
}