Frequency navigation db
This commit is contained in:
@@ -21,7 +21,7 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel<ICommandPaletteEntryV
|
||||
IIdentifiableUserCommandService identifiableUserCommandService,
|
||||
IUserCommandHandlerService userCommandHandlerService,
|
||||
ICommandKeysHelperService commandKeysHelperService,
|
||||
ILogger<CommandPaletteViewModel> logger)
|
||||
ILogger<CommandPaletteViewModel> logger) : base(logger)
|
||||
{
|
||||
_commandPaletteService = commandPaletteService;
|
||||
_identifiableUserCommandService = identifiableUserCommandService;
|
||||
@@ -34,7 +34,11 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel<ICommandPaletteEntryV
|
||||
|
||||
public void Close() => _commandPaletteService.CloseCommandPalette();
|
||||
|
||||
public override void UpdateFilteredMatches() => UpdateFilteredMatchesInternal();
|
||||
public override Task UpdateFilteredMatches()
|
||||
{
|
||||
UpdateFilteredMatchesInternal();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void UpdateFilteredMatchesInternal() =>
|
||||
FilteredMatches = _commandPaletteService
|
||||
|
||||
@@ -4,4 +4,5 @@ public interface IApplicationSettings
|
||||
{
|
||||
string AppDataRoot { get; }
|
||||
string EnvironmentName { get; }
|
||||
string DataFolderName { get; }
|
||||
}
|
||||
@@ -7,6 +7,8 @@ public class ApplicationSettings : IApplicationSettings
|
||||
public string AppDataRoot { get; private set; } = null!;
|
||||
public string EnvironmentName { get; private set; } = null!;
|
||||
|
||||
public string DataFolderName { get; } = "data";
|
||||
|
||||
public ApplicationSettings()
|
||||
{
|
||||
#if DEBUG
|
||||
|
||||
@@ -118,7 +118,7 @@ public partial class ElementPreviewViewModel : IElementPreviewViewModel, IAsyncI
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FileTime.App.Database;
|
||||
|
||||
public interface IDatabaseConnection : IDisposable
|
||||
{
|
||||
ITransaction BeginTransaction();
|
||||
IQueryCollection<T> GetCollection<T>(string collectionName);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace FileTime.App.Database;
|
||||
|
||||
public interface IDatabaseContext
|
||||
{
|
||||
ValueTask<IDatabaseConnection> GetConnectionAsync();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace FileTime.App.Database;
|
||||
|
||||
public interface ITransaction : IDisposable
|
||||
{
|
||||
ValueTask CommitAsync();
|
||||
void Rollback();
|
||||
IUpdatable<T> GetCollection<T>(string collectionName);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace FileTime.App.Database;
|
||||
|
||||
public interface IUpdatable<T>
|
||||
{
|
||||
void Insert(T item);
|
||||
void Update(T item);
|
||||
void Delete(int id);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>());
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
51
src/AppCommon/FileTime.App.Database/LiteDb/Queryable.cs
Normal file
51
src/AppCommon/FileTime.App.Database/LiteDb/Queryable.cs
Normal 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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
25
src/AppCommon/FileTime.App.Database/LiteDb/Transaction.cs
Normal file
25
src/AppCommon/FileTime.App.Database/LiteDb/Transaction.cs
Normal 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();
|
||||
}
|
||||
19
src/AppCommon/FileTime.App.Database/LiteDb/Updatable.cs
Normal file
19
src/AppCommon/FileTime.App.Database/LiteDb/Updatable.cs
Normal 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));
|
||||
}
|
||||
14
src/AppCommon/FileTime.App.Database/Startup.cs
Normal file
14
src/AppCommon/FileTime.App.Database/Startup.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using FileTime.App.Core;
|
||||
using FileTime.App.Core.Models;
|
||||
using FileTime.App.Core.Services;
|
||||
using FileTime.App.Core.Services.Persistence;
|
||||
using FileTime.App.Database;
|
||||
using FileTime.Core;
|
||||
using FileTime.Providers.Local;
|
||||
using FileTime.Providers.LocalAdmin;
|
||||
@@ -26,6 +27,7 @@ public static class DependencyInjection
|
||||
|
||||
return serviceCollection
|
||||
.AddCoreDependencies()
|
||||
.AddDatabase()
|
||||
.AddAppCoreDependencies(configuration)
|
||||
.AddLocalProviderServices()
|
||||
.AddLocalAdminProviderServices(configuration)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<ProjectReference Include="..\..\Providers\FileTime.Providers.Local\FileTime.Providers.Local.csproj" />
|
||||
<ProjectReference Include="..\..\Tools\FileTime.Tools.VirtualDiskSources\FileTime.Tools.VirtualDiskSources.csproj" />
|
||||
<ProjectReference Include="..\FileTime.App.Core\FileTime.App.Core.csproj" />
|
||||
<ProjectReference Include="..\FileTime.App.Database\FileTime.App.Database.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -9,5 +9,5 @@ public interface IFrequencyNavigationService
|
||||
IFrequencyNavigationViewModel? CurrentModal { get; }
|
||||
Task OpenNavigationWindow();
|
||||
void CloseNavigationWindow();
|
||||
IList<string> GetMatchingContainers(string searchText);
|
||||
ValueTask<IList<string>> GetMatchingContainers(string searchText);
|
||||
}
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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.Database.Abstractions\FileTime.App.Database.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\FileTime.App.FrequencyNavigation.Abstractions\FileTime.App.FrequencyNavigation.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\FileTime.App.FuzzyPanel\FileTime.App.FuzzyPanel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -2,6 +2,14 @@ namespace FileTime.App.FrequencyNavigation.Models;
|
||||
|
||||
public class ContainerFrequencyData
|
||||
{
|
||||
public int Score { get; set; } = 1;
|
||||
public DateTime LastAccessed { get; set; } = DateTime.Now;
|
||||
public string Path { get; }
|
||||
public int Score { get; set; }
|
||||
public DateTime LastAccessed { get; set; }
|
||||
|
||||
public ContainerFrequencyData(string path, int score, DateTime lastAccessed)
|
||||
{
|
||||
Path = path;
|
||||
Score = score;
|
||||
LastAccessed = lastAccessed;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,35 +9,38 @@ using FileTime.Core.Models.Extensions;
|
||||
using FileTime.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using static System.DeferTools;
|
||||
|
||||
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 AgingPersistenceAfterSeconds = 60;
|
||||
|
||||
private DateTime _lastSave = DateTime.Now;
|
||||
private readonly ILogger<FrequencyNavigationService> _logger;
|
||||
private readonly IModalService _modalService;
|
||||
private readonly SemaphoreSlim _saveLock = new(1, 1);
|
||||
private Dictionary<string, ContainerFrequencyData> _containerScores = new();
|
||||
private readonly FrequencyNavigationRepository _frequencyNavigationRepository;
|
||||
private readonly DeclarativeProperty<bool> _showWindow = new(false);
|
||||
private readonly string _dbPath;
|
||||
private bool _loaded;
|
||||
private readonly string _oldDbPath;
|
||||
private DateTime _lastTryAging = DateTime.Now;
|
||||
|
||||
[Notify] IFrequencyNavigationViewModel? _currentModal;
|
||||
[Notify] private IFrequencyNavigationViewModel? _currentModal;
|
||||
IDeclarativeProperty<bool> IFrequencyNavigationService.ShowWindow => _showWindow;
|
||||
|
||||
public FrequencyNavigationService(
|
||||
ITabEvents tabEvents,
|
||||
IModalService modalService,
|
||||
FrequencyNavigationRepository frequencyNavigationRepository,
|
||||
IApplicationSettings applicationSettings,
|
||||
ILogger<FrequencyNavigationService> logger,
|
||||
IModalService modalService)
|
||||
ILogger<FrequencyNavigationService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_modalService = modalService;
|
||||
_dbPath = Path.Combine(applicationSettings.AppDataRoot, "frequencyNavigationScores.json");
|
||||
_frequencyNavigationRepository = frequencyNavigationRepository;
|
||||
tabEvents.LocationChanged += OnTabLocationChanged;
|
||||
|
||||
_oldDbPath = Path.Combine(applicationSettings.AppDataRoot, "frequencyNavigationScores.json");
|
||||
}
|
||||
|
||||
async void OnTabLocationChanged(object? sender, TabLocationChanged e)
|
||||
@@ -70,7 +73,6 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I
|
||||
|
||||
private async Task IncreaseContainerScore(IContainer container)
|
||||
{
|
||||
await _saveLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (container.GetExtension<NonRestorableContainerExtension>() is not null) return;
|
||||
@@ -78,156 +80,87 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I
|
||||
var containerNameString = container.FullName?.Path;
|
||||
if (containerNameString is null) return;
|
||||
|
||||
if (_containerScores.ContainsKey(containerNameString))
|
||||
{
|
||||
_containerScores[containerNameString].Score++;
|
||||
_containerScores[containerNameString].LastAccessed = DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
_containerScores.Add(containerNameString, new ContainerFrequencyData());
|
||||
}
|
||||
await _frequencyNavigationRepository.IncreaseContainerScoreAsync(containerNameString);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error increasing container score");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_saveLock.Release();
|
||||
|
||||
await TryAgeContainersAsync();
|
||||
}
|
||||
|
||||
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
|
||||
await SaveStateAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
if (await _frequencyNavigationRepository.GetAgeSum() < MaxAge)
|
||||
{
|
||||
_logger.LogError(e, "Error aging container scores");
|
||||
}
|
||||
_lastTryAging = DateTime.Now;
|
||||
return;
|
||||
}
|
||||
|
||||
private bool TryAgeContainerScores()
|
||||
{
|
||||
if (_containerScores.Select(c => c.Value.Score).Sum() < MaxAge)
|
||||
return false;
|
||||
|
||||
AgeContainerScores();
|
||||
return true;
|
||||
await _frequencyNavigationRepository.AgeContainersAsync();
|
||||
}
|
||||
|
||||
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)
|
||||
public async ValueTask<IList<string>> GetMatchingContainers(string searchText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchText))
|
||||
return new List<string>();
|
||||
|
||||
_saveLock.Wait();
|
||||
try
|
||||
{
|
||||
return _containerScores
|
||||
.Where(c =>
|
||||
{
|
||||
var frequencyData = await _frequencyNavigationRepository.GetContainersAsync();
|
||||
var searchTerms = searchText.Split(' ');
|
||||
return searchTerms.All(s => c.Key.Contains(s, StringComparison.OrdinalIgnoreCase));
|
||||
})
|
||||
.OrderByDescending(c => GetWeightedScore(c.Value.Score, c.Value.LastAccessed))
|
||||
.Select(c => c.Key)
|
||||
|
||||
return frequencyData
|
||||
.Where(c => searchTerms.All(s => c.Path.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderByDescending(c => GetWeightedScore(c.Score, c.LastAccessed))
|
||||
.Select(c => c.Path)
|
||||
.ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_saveLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private int GetWeightedScore(int score, DateTime lastAccess)
|
||||
private static 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
|
||||
< 1 => score * 4,
|
||||
< 24 => score * 2,
|
||||
< 168 => score / 2,
|
||||
_ => score / 4
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: remove this migration at some time in the future
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await LoadStateAsync();
|
||||
_loaded = true;
|
||||
}
|
||||
if (!File.Exists(_oldDbPath)) return;
|
||||
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;
|
||||
|
||||
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)
|
||||
{
|
||||
if(!_loaded) return;
|
||||
await _saveLock.WaitAsync(token);
|
||||
try
|
||||
{
|
||||
_lastSave = DateTime.Now;
|
||||
await using var dbStream = File.Create(_dbPath);
|
||||
await JsonSerializer.SerializeAsync(dbStream, _containerScores);
|
||||
dbStream.Flush();
|
||||
await _frequencyNavigationRepository.AddContainersAsync(frequencyData);
|
||||
}
|
||||
finally
|
||||
|
||||
private class OldContainerFrequencyData
|
||||
{
|
||||
_saveLock.Release();
|
||||
}
|
||||
public int Score { get; set; } = 1;
|
||||
public DateTime LastAccessed { get; set; } = DateTime.Now;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public static class Startup
|
||||
services.AddSingleton<FrequencyNavigationService>();
|
||||
services.TryAddSingleton<IFrequencyNavigationService>(sp => sp.GetRequiredService<FrequencyNavigationService>());
|
||||
services.AddSingleton<IStartupHandler>(sp => sp.GetRequiredService<FrequencyNavigationService>());
|
||||
services.AddSingleton<IExitHandler>(sp => sp.GetRequiredService<FrequencyNavigationService>());
|
||||
services.TryAddTransient<FrequencyNavigationRepository>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using FileTime.App.FuzzyPanel;
|
||||
using FileTime.Core.Models;
|
||||
using FileTime.Core.Timeline;
|
||||
using GeneralInputKey;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FileTime.App.FrequencyNavigation.ViewModels;
|
||||
|
||||
@@ -18,7 +19,8 @@ public class FrequencyNavigationViewModel : FuzzyPanelViewModel<string>, IFreque
|
||||
public FrequencyNavigationViewModel(
|
||||
IFrequencyNavigationService frequencyNavigationService,
|
||||
IUserCommandHandlerService userCommandHandlerService,
|
||||
ITimelessContentProvider timelessContentProvider)
|
||||
ITimelessContentProvider timelessContentProvider,
|
||||
ILogger<FrequencyNavigationViewModel> logger) : base(logger)
|
||||
{
|
||||
_frequencyNavigationService = frequencyNavigationService;
|
||||
_userCommandHandlerService = userCommandHandlerService;
|
||||
@@ -64,8 +66,8 @@ public class FrequencyNavigationViewModel : FuzzyPanelViewModel<string>, IFreque
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void UpdateFilteredMatches() =>
|
||||
FilteredMatches = new List<string>(_frequencyNavigationService.GetMatchingContainers(SearchText));
|
||||
public override async Task UpdateFilteredMatches()
|
||||
=> FilteredMatches = new List<string>(await _frequencyNavigationService.GetMatchingContainers(SearchText));
|
||||
|
||||
string IModalViewModel.Name => "FrequencyNavigation";
|
||||
}
|
||||
@@ -7,6 +7,6 @@ public interface IFuzzyPanelViewModel<TItem> where TItem : class
|
||||
List<TItem> FilteredMatches { get; }
|
||||
TItem? SelectedItem { get; }
|
||||
string SearchText { get; set; }
|
||||
void UpdateFilteredMatches();
|
||||
Task UpdateFilteredMatches();
|
||||
Task<bool> HandleKeyDown(GeneralKeyEventArgs keyEventArgs);
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
||||
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
using System.ComponentModel;
|
||||
using DeclarativeProperty;
|
||||
using GeneralInputKey;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
|
||||
namespace FileTime.App.FuzzyPanel;
|
||||
|
||||
public abstract partial class FuzzyPanelViewModel<TItem> : IFuzzyPanelViewModel<TItem> where TItem : class
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly Func<TItem, TItem, bool> _itemEquality;
|
||||
private string _searchText = String.Empty;
|
||||
|
||||
[Notify(set: Setter.Protected)] private IDeclarativeProperty<bool> _showWindow;
|
||||
[Notify(set: Setter.Protected)] private List<TItem> _filteredMatches;
|
||||
[Notify(set: Setter.Protected)] private IDeclarativeProperty<bool> _showWindow = null!;
|
||||
[Notify(set: Setter.Protected)] private List<TItem> _filteredMatches = null!;
|
||||
[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);
|
||||
}
|
||||
|
||||
@@ -29,7 +32,15 @@ public abstract partial class FuzzyPanelViewModel<TItem> : IFuzzyPanelViewModel<
|
||||
_searchText = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(SearchText)));
|
||||
|
||||
UpdateFilteredMatches();
|
||||
Update(value);
|
||||
}
|
||||
}
|
||||
|
||||
private async void Update(string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
await UpdateFilteredMatches();
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
SelectedItem = null;
|
||||
@@ -39,6 +50,10 @@ public abstract partial class FuzzyPanelViewModel<TItem> : IFuzzyPanelViewModel<
|
||||
UpdateSelectedItem();
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while updating filtered matches");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSelectedItem()
|
||||
@@ -50,7 +65,7 @@ public abstract partial class FuzzyPanelViewModel<TItem> : IFuzzyPanelViewModel<
|
||||
: null;
|
||||
}
|
||||
|
||||
public abstract void UpdateFilteredMatches();
|
||||
public abstract Task UpdateFilteredMatches();
|
||||
|
||||
public virtual Task<bool> HandleKeyDown(GeneralKeyEventArgs keyEventArgs)
|
||||
{
|
||||
|
||||
@@ -151,6 +151,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Tools.VirtualDiskS
|
||||
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}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -488,6 +500,8 @@ Global
|
||||
{25AA9F04-EEEE-49C4-870B-CDFF71717687} = {778AAF38-20FF-438C-A9C3-60850C8B5A27}
|
||||
{DBCB10AD-9647-46AB-B9A1-3ACB9BCA46B9} = {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
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Avalonia.Input;
|
||||
using FileTime.App.Core.Configuration;
|
||||
using FileTime.App.Core.Services;
|
||||
using FileTime.App.Core.ViewModels;
|
||||
using FileTime.App.Database.LiteDb;
|
||||
using FileTime.Core.Interactions;
|
||||
using FileTime.GuiApp.App.CloudDrives;
|
||||
using FileTime.GuiApp.App.Configuration;
|
||||
|
||||
Reference in New Issue
Block a user