Frequency navigation
This commit is contained in:
@@ -11,4 +11,8 @@
|
|||||||
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.0.0-preview5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Avalonia.Input;
|
||||||
using FileTime.App.Core.ViewModels;
|
using FileTime.App.Core.ViewModels;
|
||||||
|
|
||||||
namespace FileTime.App.FrequencyNavigation.ViewModels;
|
namespace FileTime.App.FrequencyNavigation.ViewModels;
|
||||||
@@ -9,4 +10,5 @@ public interface IFrequencyNavigationViewModel : IModalViewModel
|
|||||||
string SearchText { get; set; }
|
string SearchText { get; set; }
|
||||||
string SelectedItem { get; set; }
|
string SelectedItem { get; set; }
|
||||||
void Close();
|
void Close();
|
||||||
|
void HandleKeyDown(KeyEventArgs keyEventArgs);
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I
|
|||||||
if (TryAgeContainerScores() || DateTime.Now - _lastSave > TimeSpan.FromMinutes(5))
|
if (TryAgeContainerScores() || DateTime.Now - _lastSave > TimeSpan.FromMinutes(5))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
//TODO: move to if above
|
||||||
await SaveStateAsync();
|
await SaveStateAsync();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -113,7 +113,7 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I
|
|||||||
var itemsToRemove = new List<string>();
|
var itemsToRemove = new List<string>();
|
||||||
foreach (var container in _containerScores)
|
foreach (var container in _containerScores)
|
||||||
{
|
{
|
||||||
var newScore = (int) Math.Floor(container.Value.Score * 0.9);
|
var newScore = (int)Math.Floor(container.Value.Score * 0.9);
|
||||||
if (newScore > 0)
|
if (newScore > 0)
|
||||||
{
|
{
|
||||||
container.Value.Score = newScore;
|
container.Value.Score = newScore;
|
||||||
@@ -136,14 +136,18 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I
|
|||||||
return new List<string>();
|
return new List<string>();
|
||||||
|
|
||||||
_saveLock.Wait();
|
_saveLock.Wait();
|
||||||
var matchingContainers = _containerScores
|
try
|
||||||
.Where(c => c.Key.Contains(searchText, StringComparison.OrdinalIgnoreCase))
|
{
|
||||||
.OrderBy(c => GetWeightedScore(c.Value.Score, c.Value.LastAccessed))
|
return _containerScores
|
||||||
.Select(c => c.Key)
|
.Where(c => c.Key.Contains(searchText, StringComparison.OrdinalIgnoreCase))
|
||||||
.ToList();
|
.OrderBy(c => GetWeightedScore(c.Value.Score, c.Value.LastAccessed))
|
||||||
|
.Select(c => c.Key)
|
||||||
_saveLock.Release();
|
.ToList();
|
||||||
return matchingContainers;
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_saveLock.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetWeightedScore(int score, DateTime lastAccess)
|
private int GetWeightedScore(int score, DateTime lastAccess)
|
||||||
@@ -159,36 +163,48 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync() => await LoadStateAsync();
|
||||||
{
|
|
||||||
await LoadStateAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadStateAsync()
|
private async Task LoadStateAsync()
|
||||||
{
|
{
|
||||||
if (!File.Exists(_dbPath))
|
if (!File.Exists(_dbPath))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await _saveLock.WaitAsync();
|
try
|
||||||
await using var dbStream = File.OpenRead(_dbPath);
|
{
|
||||||
var containerScores = await JsonSerializer.DeserializeAsync<Dictionary<string, ContainerFrequencyData>>(dbStream);
|
await _saveLock.WaitAsync();
|
||||||
if (containerScores is null) return;
|
_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;
|
_containerScores = containerScores;
|
||||||
_saveLock.Release();
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Error loading frequency navigation state");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_saveLock.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExitAsync()
|
public async Task ExitAsync() => await SaveStateAsync();
|
||||||
{
|
|
||||||
await SaveStateAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveStateAsync()
|
private async Task SaveStateAsync()
|
||||||
{
|
{
|
||||||
await _saveLock.WaitAsync();
|
await _saveLock.WaitAsync();
|
||||||
_lastSave = DateTime.Now;
|
try
|
||||||
await using var dbStream = File.OpenWrite(_dbPath);
|
{
|
||||||
await JsonSerializer.SerializeAsync(dbStream, _containerScores);
|
_lastSave = DateTime.Now;
|
||||||
_saveLock.Release();
|
await using var dbStream = File.Create(_dbPath);
|
||||||
|
await JsonSerializer.SerializeAsync(dbStream, _containerScores);
|
||||||
|
dbStream.Flush();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_saveLock.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
|
using Avalonia.Input;
|
||||||
|
using FileTime.App.Core.Services;
|
||||||
|
using FileTime.App.Core.UserCommand;
|
||||||
using FileTime.App.Core.ViewModels;
|
using FileTime.App.Core.ViewModels;
|
||||||
using FileTime.App.FrequencyNavigation.Services;
|
using FileTime.App.FrequencyNavigation.Services;
|
||||||
|
using FileTime.Core.Models;
|
||||||
|
using FileTime.Core.Timeline;
|
||||||
using MvvmGen;
|
using MvvmGen;
|
||||||
|
|
||||||
namespace FileTime.App.FrequencyNavigation.ViewModels;
|
namespace FileTime.App.FrequencyNavigation.ViewModels;
|
||||||
|
|
||||||
[ViewModel]
|
[ViewModel]
|
||||||
[Inject(typeof(IFrequencyNavigationService), "_frequencyNavigationService")]
|
[Inject(typeof(IFrequencyNavigationService), "_frequencyNavigationService")]
|
||||||
|
[Inject(typeof(IUserCommandHandlerService), "_userCommandHandlerService")]
|
||||||
|
[Inject(typeof(ITimelessContentProvider), "_timelessContentProvider")]
|
||||||
public partial class FrequencyNavigationViewModel : IFrequencyNavigationViewModel
|
public partial class FrequencyNavigationViewModel : IFrequencyNavigationViewModel
|
||||||
{
|
{
|
||||||
private string _searchText;
|
private string _searchText;
|
||||||
|
|
||||||
[Property] private IObservable<bool> _showWindow;
|
[Property] private IObservable<bool> _showWindow;
|
||||||
[Property] private List<string> _filteredMatches;
|
[Property] private List<string> _filteredMatches;
|
||||||
[Property] private string _selectedItem;
|
[Property] private string _selectedItem;
|
||||||
@@ -20,7 +27,7 @@ public partial class FrequencyNavigationViewModel : IFrequencyNavigationViewMode
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (_searchText == value) return;
|
if (_searchText == value) return;
|
||||||
|
|
||||||
_searchText = value;
|
_searchText = value;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
|
||||||
@@ -29,18 +36,48 @@ public partial class FrequencyNavigationViewModel : IFrequencyNavigationViewMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void Close()
|
public void Close()
|
||||||
|
=> _frequencyNavigationService.CloseNavigationWindow();
|
||||||
|
|
||||||
|
public async void HandleKeyDown(KeyEventArgs keyEventArgs)
|
||||||
{
|
{
|
||||||
_frequencyNavigationService.CloseNavigationWindow();
|
if (keyEventArgs.Key == Key.Down)
|
||||||
|
{
|
||||||
|
var nextItem = FilteredMatches.SkipWhile(i => i != SelectedItem).Skip(1).FirstOrDefault();
|
||||||
|
|
||||||
|
if (nextItem is not null)
|
||||||
|
{
|
||||||
|
SelectedItem = nextItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (keyEventArgs.Key == Key.Up)
|
||||||
|
{
|
||||||
|
var previousItem = FilteredMatches.TakeWhile(i => i != SelectedItem).LastOrDefault();
|
||||||
|
|
||||||
|
if (previousItem is not null)
|
||||||
|
{
|
||||||
|
SelectedItem = previousItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (keyEventArgs.Key == Key.Enter)
|
||||||
|
{
|
||||||
|
var targetContainer = await _timelessContentProvider.GetItemByFullNameAsync(new FullName(SelectedItem), PointInTime.Present);
|
||||||
|
var openContainerCommand = new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, targetContainer));
|
||||||
|
await _userCommandHandlerService.HandleCommandAsync(openContainerCommand);
|
||||||
|
Close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnInitialize()
|
partial void OnInitialize()
|
||||||
{
|
=> _showWindow = _frequencyNavigationService.ShowWindow;
|
||||||
_showWindow = _frequencyNavigationService.ShowWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateFilteredMatches()
|
private void UpdateFilteredMatches()
|
||||||
{
|
{
|
||||||
FilteredMatches = new List<string>(_frequencyNavigationService.GetMatchingContainers(_searchText));
|
FilteredMatches = new List<string>(_frequencyNavigationService.GetMatchingContainers(_searchText));
|
||||||
|
if (FilteredMatches.Contains(SelectedItem)) return;
|
||||||
|
|
||||||
|
SelectedItem = FilteredMatches.Count > 0
|
||||||
|
? FilteredMatches[0]
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
string IModalViewModel.Name => "FrequencyNavigation";
|
string IModalViewModel.Name => "FrequencyNavigation";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
"MinimumLevel": {
|
"MinimumLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Verbose",
|
||||||
"Override": {
|
"Override": {
|
||||||
"Microsoft": "Information",
|
"Microsoft": "Information",
|
||||||
"System": "Warning"
|
"System": "Warning"
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ public class ToastMessageSink : ILogEventSink
|
|||||||
if (logEvent.Level >= LogEventLevel.Error)
|
if (logEvent.Level >= LogEventLevel.Error)
|
||||||
{
|
{
|
||||||
var message = logEvent.RenderMessage();
|
var message = logEvent.RenderMessage();
|
||||||
|
if (logEvent.Exception is not null)
|
||||||
|
message += $" {logEvent.Exception.Message}";
|
||||||
dialogService.ShowToastMessage(message);
|
dialogService.ShowToastMessage(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,21 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="ListBox.CommandPalette">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBox.CommandPalette > ListBoxItem">
|
||||||
|
<Setter Property="Margin" Value="0" />
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBox.CommandPalette > ListBoxItem[IsSelected=true] TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource SelectedItemForegroundBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Grid.SidebarContainerPresenter">
|
<Style Selector="Grid.SidebarContainerPresenter">
|
||||||
<Setter Property="Background" Value="#01000000" />
|
<Setter Property="Background" Value="#01000000" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|||||||
@@ -9,21 +9,28 @@
|
|||||||
d:DesignWidth="800"
|
d:DesignWidth="800"
|
||||||
x:CompileBindings="True"
|
x:CompileBindings="True"
|
||||||
x:DataType="viewModels:IFrequencyNavigationViewModel"
|
x:DataType="viewModels:IFrequencyNavigationViewModel"
|
||||||
|
Background="{DynamicResource ContainerBackgroundColor}"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<StyleInclude Source="avares://FileTime.GuiApp/Resources/Styles.axaml" />
|
||||||
|
</UserControl.Styles>
|
||||||
<Grid RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,*">
|
||||||
<TextBox
|
<TextBox
|
||||||
|
Focusable="True"
|
||||||
KeyDown="Search_OnKeyDown"
|
KeyDown="Search_OnKeyDown"
|
||||||
Text="{Binding SearchText, Mode=TwoWay}" />
|
Text="{Binding SearchText, Mode=TwoWay}" />
|
||||||
<ItemsRepeater
|
<ListBox
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Items="{Binding FilteredMatches}">
|
Classes="CommandPalette"
|
||||||
<ItemsRepeater.ItemTemplate>
|
Items="{Binding FilteredMatches}"
|
||||||
|
SelectedItem="{Binding SelectedItem}">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="x:String">
|
<DataTemplate x:DataType="x:String">
|
||||||
<Grid Margin="5">
|
<Grid Margin="5">
|
||||||
<TextBlock Text="{Binding}" />
|
<TextBlock Text="{Binding}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsRepeater.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ItemsRepeater>
|
</ListBox>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@@ -26,5 +26,9 @@ public partial class FrequencyNavigation : UserControl
|
|||||||
{
|
{
|
||||||
viewModel.Close();
|
viewModel.Close();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
viewModel.HandleKeyDown(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -719,12 +719,13 @@
|
|||||||
|
|
||||||
<Border
|
<Border
|
||||||
DataContext="{Binding FrequencyNavigationService.CurrentModal}"
|
DataContext="{Binding FrequencyNavigationService.CurrentModal}"
|
||||||
Margin="100"
|
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
Background="{DynamicResource BarelyTransparentBackgroundColor}"
|
Background="{DynamicResource BarelyTransparentBackgroundColor}"
|
||||||
IsVisible="{Binding ShowWindow^, FallbackValue=False}">
|
IsVisible="{Binding ShowWindow^, FallbackValue=False}">
|
||||||
<local:FrequencyNavigation />
|
<Grid Margin="100">
|
||||||
|
<local:FrequencyNavigation />
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user