Frequency navigation db

This commit is contained in:
2023-09-12 17:25:24 +02:00
parent 98e206e6d2
commit eef783bd77
35 changed files with 726 additions and 140 deletions

View File

@@ -21,7 +21,7 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel<ICommandPaletteEntryV
IIdentifiableUserCommandService identifiableUserCommandService, IIdentifiableUserCommandService identifiableUserCommandService,
IUserCommandHandlerService userCommandHandlerService, IUserCommandHandlerService userCommandHandlerService,
ICommandKeysHelperService commandKeysHelperService, ICommandKeysHelperService commandKeysHelperService,
ILogger<CommandPaletteViewModel> logger) ILogger<CommandPaletteViewModel> logger) : base(logger)
{ {
_commandPaletteService = commandPaletteService; _commandPaletteService = commandPaletteService;
_identifiableUserCommandService = identifiableUserCommandService; _identifiableUserCommandService = identifiableUserCommandService;
@@ -34,7 +34,11 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel<ICommandPaletteEntryV
public void Close() => _commandPaletteService.CloseCommandPalette(); public void Close() => _commandPaletteService.CloseCommandPalette();
public override void UpdateFilteredMatches() => UpdateFilteredMatchesInternal(); public override Task UpdateFilteredMatches()
{
UpdateFilteredMatchesInternal();
return Task.CompletedTask;
}
private void UpdateFilteredMatchesInternal() => private void UpdateFilteredMatchesInternal() =>
FilteredMatches = _commandPaletteService FilteredMatches = _commandPaletteService

View File

@@ -4,4 +4,5 @@ public interface IApplicationSettings
{ {
string AppDataRoot { get; } string AppDataRoot { get; }
string EnvironmentName { get; } string EnvironmentName { get; }
string DataFolderName { get; }
} }

View File

@@ -7,6 +7,8 @@ public class ApplicationSettings : IApplicationSettings
public string AppDataRoot { get; private set; } = null!; public string AppDataRoot { get; private set; } = null!;
public string EnvironmentName { get; private set; } = null!; public string EnvironmentName { get; private set; } = null!;
public string DataFolderName { get; } = "data";
public ApplicationSettings() public ApplicationSettings()
{ {
#if DEBUG #if DEBUG

View File

@@ -118,7 +118,7 @@ public partial class ElementPreviewViewModel : IElementPreviewViewModel, IAsyncI
} }
catch (Exception ex) catch (Exception ex)
{ {
TextContent = $"Error while getting content of {element.FullName}. " + ex.ToString(); TextContent = $"Error while getting content of {element.FullName}. " + ex;
} }
Mode = (TextContent?.Length ?? 0) switch Mode = (TextContent?.Length ?? 0) switch

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FileTime.App.Database</RootNamespace>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,7 @@
namespace FileTime.App.Database;
public interface IDatabaseConnection : IDisposable
{
ITransaction BeginTransaction();
IQueryCollection<T> GetCollection<T>(string collectionName);
}

View File

@@ -0,0 +1,6 @@
namespace FileTime.App.Database;
public interface IDatabaseContext
{
ValueTask<IDatabaseConnection> GetConnectionAsync();
}

View File

@@ -0,0 +1,11 @@
using System.Linq.Expressions;
namespace FileTime.App.Database;
public interface IQueryCollection<T>
{
IQueryable<T> Query();
bool Exists(Expression<Func<T, bool>> predicate);
T? FirstOrDefault(Expression<Func<T, bool>> predicate);
IEnumerable<T> ToEnumerable();
}

View File

@@ -0,0 +1,14 @@
using System.Linq.Expressions;
namespace FileTime.App.Database;
public interface IQueryable<T> : IQueryableResult<T>
{
IQueryable<T> Where(Expression<Func<T, bool>> predicate);
IQueryable<T> Skip(int skip);
IQueryable<T> Take(int take);
IQueryable<T> Include<TResult>(Expression<Func<T, TResult>> selector);
IQueryableResult<TResult> Select<TResult>(Expression<Func<T, TResult>> selector);
IQueryable<T> OrderBy<TSelector>(Expression<Func<T, TSelector>> order);
IQueryable<T> OrderByDescending<TSelector>(Expression<Func<T, TSelector>> order);
}

View File

@@ -0,0 +1,14 @@
namespace FileTime.App.Database;
public interface IQueryableResult<T>
{
int Count();
bool Exists();
T First();
T? FirstOrDefault();
T Single();
T? SingleOrDefault();
IEnumerable<T> ToEnumerable();
List<T> ToList();
T[] ToArray();
}

View File

@@ -0,0 +1,8 @@
namespace FileTime.App.Database;
public interface ITransaction : IDisposable
{
ValueTask CommitAsync();
void Rollback();
IUpdatable<T> GetCollection<T>(string collectionName);
}

View File

@@ -0,0 +1,8 @@
namespace FileTime.App.Database;
public interface IUpdatable<T>
{
void Insert(T item);
void Update(T item);
void Delete(int id);
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LiteDB" Version="5.0.17" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
<ProjectReference Include="..\FileTime.App.Database.Abstractions\FileTime.App.Database.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,34 @@
using FileTime.App.Core.Models;
using LiteDB;
namespace FileTime.App.Database.LiteDb;
public class DatabaseConnection : IDatabaseConnection
{
private readonly ILiteDatabase _liteDb;
public DatabaseConnection(IApplicationSettings applicationSettings)
{
var dataFolderPath = Path.Combine(applicationSettings.AppDataRoot, applicationSettings.DataFolderName);
if (!Directory.Exists(dataFolderPath))
{
Directory.CreateDirectory(dataFolderPath);
}
var databasePath = Path.Combine(dataFolderPath, "FileTime.db");
_liteDb = new LiteDatabase($"Filename={databasePath};Mode=Shared;");
}
public ITransaction BeginTransaction()
{
_liteDb.BeginTrans();
var database = new Transaction(_liteDb);
return database;
}
public IQueryCollection<T> GetCollection<T>(string collectionName)
=> new QueryCollection<T>(_liteDb.GetCollection<T>(collectionName));
public void Dispose() => _liteDb.Dispose();
}

View File

@@ -0,0 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.App.Database.LiteDb;
public class DatabaseContext : IDatabaseContext
{
private readonly IServiceProvider _serviceProvider;
public DatabaseContext(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public ValueTask<IDatabaseConnection> GetConnectionAsync()
=> ValueTask.FromResult((IDatabaseConnection)_serviceProvider.GetRequiredService<DatabaseConnection>());
}

View File

@@ -0,0 +1,20 @@
using System.Linq.Expressions;
using LiteDB;
namespace FileTime.App.Database.LiteDb;
public class QueryCollection<T> : IQueryCollection<T>
{
private readonly ILiteCollection<T> _collection;
public QueryCollection(ILiteCollection<T> collection)
{
_collection = collection;
}
public IQueryable<T> Query() => new Queryable<T>(_collection.Query());
public bool Exists(Expression<Func<T, bool>> predicate) => _collection.Exists(predicate);
public T? FirstOrDefault(Expression<Func<T, bool>> predicate) => _collection.FindOne(predicate);
public IEnumerable<T> ToEnumerable() => _collection.FindAll();
}

View File

@@ -0,0 +1,51 @@
using System.Linq.Expressions;
using LiteDB;
namespace FileTime.App.Database.LiteDb;
public class Queryable<T> : IQueryable<T>
{
private readonly ILiteQueryable<T> _collection;
private int SkipCount { get; init; }
private int TakeCount { get; init; }
public Queryable(ILiteQueryable<T> collection)
{
_collection = collection;
}
public IQueryable<T> Where(Expression<Func<T, bool>> predicate) => new Queryable<T>(_collection.Where(predicate));
public IQueryable<T> OrderBy<TSelector>(Expression<Func<T, TSelector>> order) => new Queryable<T>(_collection.OrderBy(order));
public IQueryable<T> OrderByDescending<TSelector>(Expression<Func<T, TSelector>> order) => new Queryable<T>(_collection.OrderByDescending(order));
public IQueryable<T> Skip(int skip) => new Queryable<T>(_collection) {SkipCount = skip};
public IQueryable<T> Take(int take) => new Queryable<T>(_collection) {TakeCount = take};
public IQueryable<T> Include<TResult>(Expression<Func<T, TResult>> selector) => new Queryable<T>(_collection.Include(selector));
private ILiteQueryableResult<TCollection> ApplySkipAndTake<TCollection>(ILiteQueryableResult<TCollection> collection)
{
if (SkipCount > 0)
{
collection = collection.Skip(SkipCount);
}
if (TakeCount > 0)
{
collection = collection.Limit(TakeCount);
}
return collection;
}
public IQueryableResult<TResult> Select<TResult>(Expression<Func<T, TResult>> selector) => new QueryableResult<TResult>(ApplySkipAndTake(_collection.Select(selector)));
public int Count() => ApplySkipAndTake(_collection).Count();
public bool Exists() => ApplySkipAndTake(_collection).Exists();
public T First() => ApplySkipAndTake(_collection).First();
public T FirstOrDefault() => ApplySkipAndTake(_collection).FirstOrDefault();
public T Single() => ApplySkipAndTake(_collection).Single();
public T SingleOrDefault() => ApplySkipAndTake(_collection).SingleOrDefault();
public IEnumerable<T> ToEnumerable() => ApplySkipAndTake(_collection).ToEnumerable();
public List<T> ToList() => ApplySkipAndTake(_collection).ToList();
public T[] ToArray() => ApplySkipAndTake(_collection).ToArray();
}

View File

@@ -0,0 +1,23 @@
using LiteDB;
namespace FileTime.App.Database.LiteDb;
public class QueryableResult<T> : IQueryableResult<T>
{
private readonly ILiteQueryableResult<T> _queryableResult;
public QueryableResult(ILiteQueryableResult<T> queryableResult)
{
_queryableResult = queryableResult;
}
public int Count() => _queryableResult.Count();
public bool Exists() => _queryableResult.Exists();
public T First() => _queryableResult.First();
public T FirstOrDefault() => _queryableResult.FirstOrDefault();
public T Single() => _queryableResult.Single();
public T SingleOrDefault() => _queryableResult.SingleOrDefault();
public IEnumerable<T> ToEnumerable() => _queryableResult.ToEnumerable();
public List<T> ToList() => _queryableResult.ToList();
public T[] ToArray() => _queryableResult.ToArray();
}

View File

@@ -0,0 +1,25 @@
using LiteDB;
namespace FileTime.App.Database.LiteDb;
public class Transaction : ITransaction
{
private readonly ILiteDatabase _liteDatabase;
public Transaction(ILiteDatabase liteDatabase)
{
_liteDatabase = liteDatabase;
}
public ValueTask CommitAsync()
{
_liteDatabase.Commit();
return ValueTask.CompletedTask;
}
public void Rollback() => _liteDatabase.Rollback();
public IUpdatable<T> GetCollection<T>(string collectionName) => new Updatable<T>(_liteDatabase.GetCollection<T>(collectionName));
public void Dispose() => _liteDatabase.Dispose();
}

View File

@@ -0,0 +1,19 @@
using LiteDB;
namespace FileTime.App.Database.LiteDb;
public class Updatable<T> : IUpdatable<T>
{
private readonly ILiteCollection<T> _collection;
public Updatable(ILiteCollection<T> collection)
{
_collection = collection;
}
public void Insert(T item) => _collection.Insert(item);
public void Update(T item) => _collection.Update(item);
public void Delete(int id) => _collection.Delete(new BsonValue(id));
}

View File

@@ -0,0 +1,14 @@
using FileTime.App.Database.LiteDb;
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.App.Database;
public static class Startup
{
public static IServiceCollection AddDatabase(this IServiceCollection services)
{
services.AddSingleton<IDatabaseContext, DatabaseContext>();
services.AddTransient<DatabaseConnection>();
return services;
}
}

View File

@@ -2,6 +2,7 @@ using FileTime.App.Core;
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.App.Core.Services.Persistence; using FileTime.App.Core.Services.Persistence;
using FileTime.App.Database;
using FileTime.Core; using FileTime.Core;
using FileTime.Providers.Local; using FileTime.Providers.Local;
using FileTime.Providers.LocalAdmin; using FileTime.Providers.LocalAdmin;
@@ -26,6 +27,7 @@ public static class DependencyInjection
return serviceCollection return serviceCollection
.AddCoreDependencies() .AddCoreDependencies()
.AddDatabase()
.AddAppCoreDependencies(configuration) .AddAppCoreDependencies(configuration)
.AddLocalProviderServices() .AddLocalProviderServices()
.AddLocalAdminProviderServices(configuration) .AddLocalAdminProviderServices(configuration)

View File

@@ -22,6 +22,7 @@
<ProjectReference Include="..\..\Providers\FileTime.Providers.Local\FileTime.Providers.Local.csproj" /> <ProjectReference Include="..\..\Providers\FileTime.Providers.Local\FileTime.Providers.Local.csproj" />
<ProjectReference Include="..\..\Tools\FileTime.Tools.VirtualDiskSources\FileTime.Tools.VirtualDiskSources.csproj" /> <ProjectReference Include="..\..\Tools\FileTime.Tools.VirtualDiskSources\FileTime.Tools.VirtualDiskSources.csproj" />
<ProjectReference Include="..\FileTime.App.Core\FileTime.App.Core.csproj" /> <ProjectReference Include="..\FileTime.App.Core\FileTime.App.Core.csproj" />
<ProjectReference Include="..\FileTime.App.Database\FileTime.App.Database.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -9,5 +9,5 @@ public interface IFrequencyNavigationService
IFrequencyNavigationViewModel? CurrentModal { get; } IFrequencyNavigationViewModel? CurrentModal { get; }
Task OpenNavigationWindow(); Task OpenNavigationWindow();
void CloseNavigationWindow(); void CloseNavigationWindow();
IList<string> GetMatchingContainers(string searchText); ValueTask<IList<string>> GetMatchingContainers(string searchText);
} }

View File

@@ -8,7 +8,9 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" /> <ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
<ProjectReference Include="..\..\Library\Defer\Defer.csproj" />
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" /> <ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
<ProjectReference Include="..\FileTime.App.Database.Abstractions\FileTime.App.Database.Abstractions.csproj" />
<ProjectReference Include="..\FileTime.App.FrequencyNavigation.Abstractions\FileTime.App.FrequencyNavigation.Abstractions.csproj" /> <ProjectReference Include="..\FileTime.App.FrequencyNavigation.Abstractions\FileTime.App.FrequencyNavigation.Abstractions.csproj" />
<ProjectReference Include="..\FileTime.App.FuzzyPanel\FileTime.App.FuzzyPanel.csproj" /> <ProjectReference Include="..\FileTime.App.FuzzyPanel\FileTime.App.FuzzyPanel.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -2,6 +2,14 @@ namespace FileTime.App.FrequencyNavigation.Models;
public class ContainerFrequencyData public class ContainerFrequencyData
{ {
public int Score { get; set; } = 1; public string Path { get; }
public DateTime LastAccessed { get; set; } = DateTime.Now; public int Score { get; set; }
public DateTime LastAccessed { get; set; }
public ContainerFrequencyData(string path, int score, DateTime lastAccessed)
{
Path = path;
Score = score;
LastAccessed = lastAccessed;
}
} }

View File

@@ -0,0 +1,301 @@
using FileTime.App.Database;
using FileTime.App.FrequencyNavigation.Models;
using FileTime.Core.Models;
namespace FileTime.App.FrequencyNavigation.Services;
public class FrequencyNavigationRepository
{
private class ContainerScore
{
public int Id { get; set; }
public required string Path { get; init; }
public required DateTime LastAccessed { get; set; }
public required int Score { get; set; }
}
private const string CollectionName = "FrequencyNavigation";
private const double CacheExpirationInSeconds = 60;
private const double MaxPersistIntervalInSeconds = 60;
private readonly IDatabaseContext _databaseContext;
private readonly SemaphoreSlim _databaseSemaphore = new(1, 1);
private readonly SemaphoreSlim _cacheSemaphore = new(1, 1);
private readonly List<ContainerFrequencyData> _cachedFrequencyData = new();
private readonly List<ContainerFrequencyData> _extraCachedFrequencyData = new();
private DateTime _cachedTime;
private DateTime _lastPersistTime = DateTime.Now;
public FrequencyNavigationRepository(IDatabaseContext databaseContext)
{
_databaseContext = databaseContext;
}
public async Task IncreaseContainerScoreAsync(string containerNameString)
{
if (!containerNameString.Contains(Constants.SeparatorChar)) return;
await _cacheSemaphore.WaitAsync();
try
{
var frequencyData = _extraCachedFrequencyData.FirstOrDefault(d => d.Path == containerNameString);
if (frequencyData is null)
{
_extraCachedFrequencyData.Add(new ContainerFrequencyData(containerNameString, 1, DateTime.Now));
}
else
{
frequencyData.Score++;
frequencyData.LastAccessed = DateTime.Now;
}
}
finally
{
_cacheSemaphore.Release();
}
await TryPersistExtraFrequencyDataAsync();
}
private async Task TryPersistExtraFrequencyDataAsync()
{
await _cacheSemaphore.WaitAsync();
try
{
if ((DateTime.Now - _lastPersistTime).TotalSeconds < MaxPersistIntervalInSeconds)
{
return;
}
_lastPersistTime = DateTime.Now;
}
finally
{
_cacheSemaphore.Release();
}
await PersistExtraFrequencyDataAsync();
}
private async Task PersistExtraFrequencyDataAsync(bool skipCacheLock = false)
{
await _databaseSemaphore.WaitAsync();
if (!skipCacheLock)
{
await _cacheSemaphore.WaitAsync();
}
try
{
using var connection = await _databaseContext.GetConnectionAsync();
using var transaction = connection.BeginTransaction();
var queryCollection = connection.GetCollection<ContainerScore>(CollectionName);
var updateCollection = transaction.GetCollection<ContainerScore>(CollectionName);
var extraCachedFrequencyData = _extraCachedFrequencyData.ToList();
_extraCachedFrequencyData.Clear();
foreach (var extraFrequencyData in extraCachedFrequencyData)
{
var currentFrequencyData = queryCollection.FirstOrDefault(d => d.Path == extraFrequencyData.Path);
if (currentFrequencyData is null)
{
updateCollection.Insert(new ContainerScore
{
Path = extraFrequencyData.Path,
LastAccessed = extraFrequencyData.LastAccessed,
Score = extraFrequencyData.Score
});
}
else
{
currentFrequencyData.Score += extraFrequencyData.Score;
currentFrequencyData.LastAccessed = extraFrequencyData.LastAccessed;
updateCollection.Update(currentFrequencyData);
}
}
await transaction.CommitAsync();
}
finally
{
_databaseSemaphore.Release();
if (!skipCacheLock)
{
_cacheSemaphore.Release();
}
}
}
public async Task<int> GetAgeSum()
{
await _databaseSemaphore.WaitAsync();
try
{
using var connection = await _databaseContext.GetConnectionAsync();
var query = connection.GetCollection<ContainerScore>(CollectionName);
return query.Query().Select(c => c.Score).ToEnumerable().Sum();
}
finally
{
_databaseSemaphore.Release();
}
}
public async Task AgeContainersAsync()
{
await PersistExtraFrequencyDataAsync();
await _databaseSemaphore.WaitAsync();
try
{
using var connection = await _databaseContext.GetConnectionAsync();
using var transaction = connection.BeginTransaction();
var queryCollection = connection.GetCollection<ContainerScore>(CollectionName);
var updateCollection = transaction.GetCollection<ContainerScore>(CollectionName);
var now = DateTime.Now;
foreach (var container in queryCollection.ToEnumerable())
{
var newScore = (int) Math.Floor(container.Score * 0.9);
if (newScore > 0)
{
container.Score = newScore;
container.LastAccessed = now;
updateCollection.Update(container);
}
else
{
updateCollection.Delete(container.Id);
}
}
await transaction.CommitAsync();
}
finally
{
_databaseSemaphore.Release();
}
}
public async ValueTask<ICollection<ContainerFrequencyData>> GetContainersAsync()
{
await _cacheSemaphore.WaitAsync();
try
{
if ((DateTime.Now - _cachedTime).TotalSeconds > CacheExpirationInSeconds)
{
await PersistExtraFrequencyDataAsync(skipCacheLock: true);
var containerScores = await GetContainersFromDatabaseAsync();
_cachedFrequencyData.Clear();
_cachedFrequencyData.AddRange(containerScores);
_cachedTime = DateTime.Now;
return _cachedFrequencyData.ToArray();
}
var frequencyData = new List<ContainerFrequencyData>(_cachedFrequencyData);
foreach (var extraFrequencyData in _extraCachedFrequencyData)
{
var existingFrequencyData = frequencyData
.FirstOrDefault(f => f.Path == extraFrequencyData.Path);
if (existingFrequencyData is null)
{
frequencyData.Add(extraFrequencyData);
}
else
{
existingFrequencyData.Score += extraFrequencyData.Score;
existingFrequencyData.LastAccessed = extraFrequencyData.LastAccessed;
}
}
return frequencyData;
}
finally
{
_cacheSemaphore.Release();
}
}
private async Task<IEnumerable<ContainerFrequencyData>> GetContainersFromDatabaseAsync()
{
await _databaseSemaphore.WaitAsync();
try
{
using var connection = await _databaseContext.GetConnectionAsync();
var repo = connection.GetCollection<ContainerScore>(CollectionName);
return repo
.ToEnumerable()
.Select(c => new ContainerFrequencyData(c.Path, c.Score, c.LastAccessed))
.ToList();
}
finally
{
_databaseSemaphore.Release();
}
}
public async Task AddContainerAsync(ContainerFrequencyData containerFrequencyData)
{
await _databaseSemaphore.WaitAsync();
try
{
using var connection = await _databaseContext.GetConnectionAsync();
using var transaction = connection.BeginTransaction();
var repo = transaction.GetCollection<ContainerScore>(CollectionName);
repo.Insert(new ContainerScore
{
Path = containerFrequencyData.Path,
LastAccessed = containerFrequencyData.LastAccessed,
Score = containerFrequencyData.Score
});
await transaction.CommitAsync();
}
finally
{
_databaseSemaphore.Release();
}
}
public async Task AddContainersAsync(IEnumerable<ContainerFrequencyData> containerFrequencyData)
{
await _databaseSemaphore.WaitAsync();
try
{
var containerFrequencyDataList = containerFrequencyData.ToList();
if (containerFrequencyDataList.Count == 0) return;
using var connection = await _databaseContext.GetConnectionAsync();
using var transaction = connection.BeginTransaction();
var repo = transaction.GetCollection<ContainerScore>(CollectionName);
foreach (var frequencyData in containerFrequencyDataList)
{
repo.Insert(new ContainerScore
{
Path = frequencyData.Path,
LastAccessed = frequencyData.LastAccessed,
Score = frequencyData.Score
});
}
await transaction.CommitAsync();
}
finally
{
_databaseSemaphore.Release();
}
}
}

View File

@@ -9,35 +9,38 @@ using FileTime.Core.Models.Extensions;
using FileTime.Core.Services; using FileTime.Core.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using static System.DeferTools;
namespace FileTime.App.FrequencyNavigation.Services; namespace FileTime.App.FrequencyNavigation.Services;
public partial class FrequencyNavigationService : IFrequencyNavigationService, IStartupHandler, IExitHandler public partial class FrequencyNavigationService : IFrequencyNavigationService, IStartupHandler
{ {
private const int MaxAge = 10_000; private const int MaxAge = 10_000;
private const int AgingPersistenceAfterSeconds = 60;
private DateTime _lastSave = DateTime.Now;
private readonly ILogger<FrequencyNavigationService> _logger; private readonly ILogger<FrequencyNavigationService> _logger;
private readonly IModalService _modalService; private readonly IModalService _modalService;
private readonly SemaphoreSlim _saveLock = new(1, 1); private readonly FrequencyNavigationRepository _frequencyNavigationRepository;
private Dictionary<string, ContainerFrequencyData> _containerScores = new();
private readonly DeclarativeProperty<bool> _showWindow = new(false); private readonly DeclarativeProperty<bool> _showWindow = new(false);
private readonly string _dbPath; private readonly string _oldDbPath;
private bool _loaded; private DateTime _lastTryAging = DateTime.Now;
[Notify] IFrequencyNavigationViewModel? _currentModal; [Notify] private IFrequencyNavigationViewModel? _currentModal;
IDeclarativeProperty<bool> IFrequencyNavigationService.ShowWindow => _showWindow; IDeclarativeProperty<bool> IFrequencyNavigationService.ShowWindow => _showWindow;
public FrequencyNavigationService( public FrequencyNavigationService(
ITabEvents tabEvents, ITabEvents tabEvents,
IModalService modalService,
FrequencyNavigationRepository frequencyNavigationRepository,
IApplicationSettings applicationSettings, IApplicationSettings applicationSettings,
ILogger<FrequencyNavigationService> logger, ILogger<FrequencyNavigationService> logger)
IModalService modalService)
{ {
_logger = logger; _logger = logger;
_modalService = modalService; _modalService = modalService;
_dbPath = Path.Combine(applicationSettings.AppDataRoot, "frequencyNavigationScores.json"); _frequencyNavigationRepository = frequencyNavigationRepository;
tabEvents.LocationChanged += OnTabLocationChanged; tabEvents.LocationChanged += OnTabLocationChanged;
_oldDbPath = Path.Combine(applicationSettings.AppDataRoot, "frequencyNavigationScores.json");
} }
async void OnTabLocationChanged(object? sender, TabLocationChanged e) async void OnTabLocationChanged(object? sender, TabLocationChanged e)
@@ -70,7 +73,6 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I
private async Task IncreaseContainerScore(IContainer container) private async Task IncreaseContainerScore(IContainer container)
{ {
await _saveLock.WaitAsync();
try try
{ {
if (container.GetExtension<NonRestorableContainerExtension>() is not null) return; if (container.GetExtension<NonRestorableContainerExtension>() is not null) return;
@@ -78,156 +80,87 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I
var containerNameString = container.FullName?.Path; var containerNameString = container.FullName?.Path;
if (containerNameString is null) return; if (containerNameString is null) return;
if (_containerScores.ContainsKey(containerNameString)) await _frequencyNavigationRepository.IncreaseContainerScoreAsync(containerNameString);
{
_containerScores[containerNameString].Score++;
_containerScores[containerNameString].LastAccessed = DateTime.Now;
}
else
{
_containerScores.Add(containerNameString, new ContainerFrequencyData());
}
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "Error increasing container score"); _logger.LogError(e, "Error increasing container score");
} }
finally
{ await TryAgeContainersAsync();
_saveLock.Release();
} }
try private async Task TryAgeContainersAsync()
{ {
if (TryAgeContainerScores() || DateTime.Now - _lastSave > TimeSpan.FromMinutes(5)) if ((DateTime.Now - _lastTryAging).TotalSeconds >= AgingPersistenceAfterSeconds)
{ {
return;
} }
//TODO: move to if above if (await _frequencyNavigationRepository.GetAgeSum() < MaxAge)
await SaveStateAsync();
}
catch (Exception e)
{ {
_logger.LogError(e, "Error aging container scores"); _lastTryAging = DateTime.Now;
} return;
} }
private bool TryAgeContainerScores() await _frequencyNavigationRepository.AgeContainersAsync();
{
if (_containerScores.Select(c => c.Value.Score).Sum() < MaxAge)
return false;
AgeContainerScores();
return true;
} }
private void AgeContainerScores() public async ValueTask<IList<string>> GetMatchingContainers(string searchText)
{
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)) if (string.IsNullOrWhiteSpace(searchText))
return new List<string>(); return new List<string>();
_saveLock.Wait(); var frequencyData = await _frequencyNavigationRepository.GetContainersAsync();
try
{
return _containerScores
.Where(c =>
{
var searchTerms = searchText.Split(' '); var searchTerms = searchText.Split(' ');
return searchTerms.All(s => c.Key.Contains(s, StringComparison.OrdinalIgnoreCase));
}) return frequencyData
.OrderByDescending(c => GetWeightedScore(c.Value.Score, c.Value.LastAccessed)) .Where(c => searchTerms.All(s => c.Path.Contains(s, StringComparison.OrdinalIgnoreCase)))
.Select(c => c.Key) .OrderByDescending(c => GetWeightedScore(c.Score, c.LastAccessed))
.Select(c => c.Path)
.ToList(); .ToList();
} }
finally
{
_saveLock.Release();
}
}
private int GetWeightedScore(int score, DateTime lastAccess) private static int GetWeightedScore(int score, DateTime lastAccess)
{ {
var now = DateTime.Now; var now = DateTime.Now;
var timeSinceLastAccess = now - lastAccess; var timeSinceLastAccess = now - lastAccess;
return timeSinceLastAccess.TotalHours switch return timeSinceLastAccess.TotalHours switch
{ {
< 1 => score *= 4, < 1 => score * 4,
< 24 => score *= 2, < 24 => score * 2,
< 168 => score /= 2, < 168 => score / 2,
_ => score /= 4 _ => score / 4
}; };
} }
// TODO: remove this migration at some time in the future
public async Task InitAsync() public async Task InitAsync()
{ {
await LoadStateAsync(); if (!File.Exists(_oldDbPath)) return;
_loaded = true; using var _ = Defer(() => File.Delete(_oldDbPath));
}
private async Task LoadStateAsync() if ((await _frequencyNavigationRepository.GetContainersAsync()).Any()) return;
await using var dbStream = File.OpenRead(_oldDbPath);
var containerScores = await JsonSerializer.DeserializeAsync<Dictionary<string, OldContainerFrequencyData>>(dbStream);
if (containerScores is null || containerScores.Count == 0)
{ {
if (!File.Exists(_dbPath))
return; return;
try
{
await _saveLock.WaitAsync();
_logger.LogTrace("Loading frequency navigation state from file '{DbPath}'", _dbPath);
await using var dbStream = File.OpenRead(_dbPath);
var containerScores = await JsonSerializer.DeserializeAsync<Dictionary<string, ContainerFrequencyData>>(dbStream);
if (containerScores is null) return;
_containerScores = containerScores;
}
catch (Exception e)
{
_logger.LogError(e, "Error loading frequency navigation state");
}
finally
{
_saveLock.Release();
}
} }
public async Task ExitAsync(CancellationToken token = default) => await SaveStateAsync(token); var frequencyData = containerScores
.Select(
c => new ContainerFrequencyData(c.Key, c.Value.Score, c.Value.LastAccessed)
)
.Where(c => c.Path.Contains(Constants.SeparatorChar));
private async Task SaveStateAsync(CancellationToken token = default) await _frequencyNavigationRepository.AddContainersAsync(frequencyData);
}
private class OldContainerFrequencyData
{ {
if(!_loaded) return; public int Score { get; set; } = 1;
await _saveLock.WaitAsync(token); public DateTime LastAccessed { get; set; } = DateTime.Now;
try
{
_lastSave = DateTime.Now;
await using var dbStream = File.Create(_dbPath);
await JsonSerializer.SerializeAsync(dbStream, _containerScores);
dbStream.Flush();
}
finally
{
_saveLock.Release();
}
} }
} }

View File

@@ -14,7 +14,7 @@ public static class Startup
services.AddSingleton<FrequencyNavigationService>(); services.AddSingleton<FrequencyNavigationService>();
services.TryAddSingleton<IFrequencyNavigationService>(sp => sp.GetRequiredService<FrequencyNavigationService>()); services.TryAddSingleton<IFrequencyNavigationService>(sp => sp.GetRequiredService<FrequencyNavigationService>());
services.AddSingleton<IStartupHandler>(sp => sp.GetRequiredService<FrequencyNavigationService>()); services.AddSingleton<IStartupHandler>(sp => sp.GetRequiredService<FrequencyNavigationService>());
services.AddSingleton<IExitHandler>(sp => sp.GetRequiredService<FrequencyNavigationService>()); services.TryAddTransient<FrequencyNavigationRepository>();
return services; return services;
} }
} }

View File

@@ -6,6 +6,7 @@ using FileTime.App.FuzzyPanel;
using FileTime.Core.Models; using FileTime.Core.Models;
using FileTime.Core.Timeline; using FileTime.Core.Timeline;
using GeneralInputKey; using GeneralInputKey;
using Microsoft.Extensions.Logging;
namespace FileTime.App.FrequencyNavigation.ViewModels; namespace FileTime.App.FrequencyNavigation.ViewModels;
@@ -18,7 +19,8 @@ public class FrequencyNavigationViewModel : FuzzyPanelViewModel<string>, IFreque
public FrequencyNavigationViewModel( public FrequencyNavigationViewModel(
IFrequencyNavigationService frequencyNavigationService, IFrequencyNavigationService frequencyNavigationService,
IUserCommandHandlerService userCommandHandlerService, IUserCommandHandlerService userCommandHandlerService,
ITimelessContentProvider timelessContentProvider) ITimelessContentProvider timelessContentProvider,
ILogger<FrequencyNavigationViewModel> logger) : base(logger)
{ {
_frequencyNavigationService = frequencyNavigationService; _frequencyNavigationService = frequencyNavigationService;
_userCommandHandlerService = userCommandHandlerService; _userCommandHandlerService = userCommandHandlerService;
@@ -64,8 +66,8 @@ public class FrequencyNavigationViewModel : FuzzyPanelViewModel<string>, IFreque
return false; return false;
} }
public override void UpdateFilteredMatches() => public override async Task UpdateFilteredMatches()
FilteredMatches = new List<string>(_frequencyNavigationService.GetMatchingContainers(SearchText)); => FilteredMatches = new List<string>(await _frequencyNavigationService.GetMatchingContainers(SearchText));
string IModalViewModel.Name => "FrequencyNavigation"; string IModalViewModel.Name => "FrequencyNavigation";
} }

View File

@@ -7,6 +7,6 @@ public interface IFuzzyPanelViewModel<TItem> where TItem : class
List<TItem> FilteredMatches { get; } List<TItem> FilteredMatches { get; }
TItem? SelectedItem { get; } TItem? SelectedItem { get; }
string SearchText { get; set; } string SearchText { get; set; }
void UpdateFilteredMatches(); Task UpdateFilteredMatches();
Task<bool> HandleKeyDown(GeneralKeyEventArgs keyEventArgs); Task<bool> HandleKeyDown(GeneralKeyEventArgs keyEventArgs);
} }

View File

@@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8"> <PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,21 +1,24 @@
using System.ComponentModel; using System.ComponentModel;
using DeclarativeProperty; using DeclarativeProperty;
using GeneralInputKey; using GeneralInputKey;
using Microsoft.Extensions.Logging;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
namespace FileTime.App.FuzzyPanel; namespace FileTime.App.FuzzyPanel;
public abstract partial class FuzzyPanelViewModel<TItem> : IFuzzyPanelViewModel<TItem> where TItem : class public abstract partial class FuzzyPanelViewModel<TItem> : IFuzzyPanelViewModel<TItem> where TItem : class
{ {
private readonly ILogger _logger;
private readonly Func<TItem, TItem, bool> _itemEquality; private readonly Func<TItem, TItem, bool> _itemEquality;
private string _searchText = String.Empty; private string _searchText = String.Empty;
[Notify(set: Setter.Protected)] private IDeclarativeProperty<bool> _showWindow; [Notify(set: Setter.Protected)] private IDeclarativeProperty<bool> _showWindow = null!;
[Notify(set: Setter.Protected)] private List<TItem> _filteredMatches; [Notify(set: Setter.Protected)] private List<TItem> _filteredMatches = null!;
[Notify(set: Setter.Protected)] private TItem? _selectedItem; [Notify(set: Setter.Protected)] private TItem? _selectedItem;
protected FuzzyPanelViewModel(Func<TItem, TItem, bool>? itemEquality = null) protected FuzzyPanelViewModel(ILogger logger, Func<TItem, TItem, bool>? itemEquality = null)
{ {
_logger = logger;
_itemEquality = itemEquality ?? ((a, b) => a == b); _itemEquality = itemEquality ?? ((a, b) => a == b);
} }
@@ -29,7 +32,15 @@ public abstract partial class FuzzyPanelViewModel<TItem> : IFuzzyPanelViewModel<
_searchText = value; _searchText = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(SearchText))); OnPropertyChanged(new PropertyChangedEventArgs(nameof(SearchText)));
UpdateFilteredMatches(); Update(value);
}
}
private async void Update(string value)
{
try
{
await UpdateFilteredMatches();
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
{ {
SelectedItem = null; SelectedItem = null;
@@ -39,6 +50,10 @@ public abstract partial class FuzzyPanelViewModel<TItem> : IFuzzyPanelViewModel<
UpdateSelectedItem(); UpdateSelectedItem();
} }
} }
catch(Exception e)
{
_logger.LogError(e, "Error while updating filtered matches");
}
} }
private void UpdateSelectedItem() private void UpdateSelectedItem()
@@ -50,7 +65,7 @@ public abstract partial class FuzzyPanelViewModel<TItem> : IFuzzyPanelViewModel<
: null; : null;
} }
public abstract void UpdateFilteredMatches(); public abstract Task UpdateFilteredMatches();
public virtual Task<bool> HandleKeyDown(GeneralKeyEventArgs keyEventArgs) public virtual Task<bool> HandleKeyDown(GeneralKeyEventArgs keyEventArgs)
{ {

View File

@@ -151,6 +151,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Tools.VirtualDiskS
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Tools.VirtualDiskSources.Abstractions", "Tools\FileTime.Tools.VirtualDiskSources.Abstractions\FileTime.Tools.VirtualDiskSources.Abstractions.csproj", "{53E5B762-B620-4106-B481-31A478A1E14F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Tools.VirtualDiskSources.Abstractions", "Tools\FileTime.Tools.VirtualDiskSources.Abstractions\FileTime.Tools.VirtualDiskSources.Abstractions.csproj", "{53E5B762-B620-4106-B481-31A478A1E14F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.App.Database", "AppCommon\FileTime.App.Database\FileTime.App.Database.csproj", "{610C9140-4B05-46A2-BFF4-501049EBA25E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.App.Database.Abstractions", "AppCommon\FileTime.App.Database.Abstractions\FileTime.App.Database.Abstractions.csproj", "{635DC6E5-A762-409E-BBCC-CE1D29F4DDB9}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -417,6 +421,14 @@ Global
{53E5B762-B620-4106-B481-31A478A1E14F}.Debug|Any CPU.Build.0 = Debug|Any CPU {53E5B762-B620-4106-B481-31A478A1E14F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53E5B762-B620-4106-B481-31A478A1E14F}.Release|Any CPU.ActiveCfg = Release|Any CPU {53E5B762-B620-4106-B481-31A478A1E14F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53E5B762-B620-4106-B481-31A478A1E14F}.Release|Any CPU.Build.0 = Release|Any CPU {53E5B762-B620-4106-B481-31A478A1E14F}.Release|Any CPU.Build.0 = Release|Any CPU
{610C9140-4B05-46A2-BFF4-501049EBA25E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{610C9140-4B05-46A2-BFF4-501049EBA25E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{610C9140-4B05-46A2-BFF4-501049EBA25E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{610C9140-4B05-46A2-BFF4-501049EBA25E}.Release|Any CPU.Build.0 = Release|Any CPU
{635DC6E5-A762-409E-BBCC-CE1D29F4DDB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{635DC6E5-A762-409E-BBCC-CE1D29F4DDB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{635DC6E5-A762-409E-BBCC-CE1D29F4DDB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{635DC6E5-A762-409E-BBCC-CE1D29F4DDB9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -488,6 +500,8 @@ Global
{25AA9F04-EEEE-49C4-870B-CDFF71717687} = {778AAF38-20FF-438C-A9C3-60850C8B5A27} {25AA9F04-EEEE-49C4-870B-CDFF71717687} = {778AAF38-20FF-438C-A9C3-60850C8B5A27}
{DBCB10AD-9647-46AB-B9A1-3ACB9BCA46B9} = {8C3CFEFE-78A5-4940-B388-D15FCE02ECE9} {DBCB10AD-9647-46AB-B9A1-3ACB9BCA46B9} = {8C3CFEFE-78A5-4940-B388-D15FCE02ECE9}
{53E5B762-B620-4106-B481-31A478A1E14F} = {8C3CFEFE-78A5-4940-B388-D15FCE02ECE9} {53E5B762-B620-4106-B481-31A478A1E14F} = {8C3CFEFE-78A5-4940-B388-D15FCE02ECE9}
{610C9140-4B05-46A2-BFF4-501049EBA25E} = {A5291117-3001-498B-AC8B-E14F71F72570}
{635DC6E5-A762-409E-BBCC-CE1D29F4DDB9} = {A5291117-3001-498B-AC8B-E14F71F72570}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}

View File

@@ -4,6 +4,7 @@ using Avalonia.Input;
using FileTime.App.Core.Configuration; using FileTime.App.Core.Configuration;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.App.Database.LiteDb;
using FileTime.Core.Interactions; using FileTime.Core.Interactions;
using FileTime.GuiApp.App.CloudDrives; using FileTime.GuiApp.App.CloudDrives;
using FileTime.GuiApp.App.Configuration; using FileTime.GuiApp.App.Configuration;