diff --git a/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/FileTime.App.FrequencyNavigation.Abstractions.csproj b/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/FileTime.App.FrequencyNavigation.Abstractions.csproj index 1e26cb7..76e0183 100644 --- a/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/FileTime.App.FrequencyNavigation.Abstractions.csproj +++ b/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/FileTime.App.FrequencyNavigation.Abstractions.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs b/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs index a20f81a..7fd7a3f 100644 --- a/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs +++ b/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs @@ -1,3 +1,4 @@ +using Avalonia.Input; using FileTime.App.Core.ViewModels; namespace FileTime.App.FrequencyNavigation.ViewModels; @@ -9,4 +10,5 @@ public interface IFrequencyNavigationViewModel : IModalViewModel string SearchText { get; set; } string SelectedItem { get; set; } void Close(); + void HandleKeyDown(KeyEventArgs keyEventArgs); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs b/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs index 5538bba..7fad564 100644 --- a/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs +++ b/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs @@ -89,7 +89,7 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I if (TryAgeContainerScores() || DateTime.Now - _lastSave > TimeSpan.FromMinutes(5)) { } - + //TODO: move to if above await SaveStateAsync(); } catch (Exception e) @@ -113,7 +113,7 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I var itemsToRemove = new List(); 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) { container.Value.Score = newScore; @@ -136,14 +136,18 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I return new List(); _saveLock.Wait(); - var matchingContainers = _containerScores - .Where(c => c.Key.Contains(searchText, StringComparison.OrdinalIgnoreCase)) - .OrderBy(c => GetWeightedScore(c.Value.Score, c.Value.LastAccessed)) - .Select(c => c.Key) - .ToList(); - - _saveLock.Release(); - return matchingContainers; + try + { + return _containerScores + .Where(c => c.Key.Contains(searchText, StringComparison.OrdinalIgnoreCase)) + .OrderBy(c => GetWeightedScore(c.Value.Score, c.Value.LastAccessed)) + .Select(c => c.Key) + .ToList(); + } + finally + { + _saveLock.Release(); + } } private int GetWeightedScore(int score, DateTime lastAccess) @@ -159,36 +163,48 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I }; } - public async Task InitAsync() - { - await LoadStateAsync(); - } + public async Task InitAsync() => await LoadStateAsync(); private async Task LoadStateAsync() { if (!File.Exists(_dbPath)) return; - await _saveLock.WaitAsync(); - await using var dbStream = File.OpenRead(_dbPath); - var containerScores = await JsonSerializer.DeserializeAsync>(dbStream); - if (containerScores is null) 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>(dbStream); + if (containerScores is null) return; - _containerScores = containerScores; - _saveLock.Release(); + _containerScores = containerScores; + } + catch (Exception e) + { + _logger.LogError(e, "Error loading frequency navigation state"); + } + finally + { + _saveLock.Release(); + } } - public async Task ExitAsync() - { - await SaveStateAsync(); - } + public async Task ExitAsync() => await SaveStateAsync(); private async Task SaveStateAsync() { await _saveLock.WaitAsync(); - _lastSave = DateTime.Now; - await using var dbStream = File.OpenWrite(_dbPath); - await JsonSerializer.SerializeAsync(dbStream, _containerScores); - _saveLock.Release(); + try + { + _lastSave = DateTime.Now; + await using var dbStream = File.Create(_dbPath); + await JsonSerializer.SerializeAsync(dbStream, _containerScores); + dbStream.Flush(); + } + finally + { + _saveLock.Release(); + } } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs b/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs index fb242ba..0403f4f 100644 --- a/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs +++ b/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs @@ -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.FrequencyNavigation.Services; +using FileTime.Core.Models; +using FileTime.Core.Timeline; using MvvmGen; namespace FileTime.App.FrequencyNavigation.ViewModels; [ViewModel] [Inject(typeof(IFrequencyNavigationService), "_frequencyNavigationService")] +[Inject(typeof(IUserCommandHandlerService), "_userCommandHandlerService")] +[Inject(typeof(ITimelessContentProvider), "_timelessContentProvider")] public partial class FrequencyNavigationViewModel : IFrequencyNavigationViewModel { private string _searchText; - + [Property] private IObservable _showWindow; [Property] private List _filteredMatches; [Property] private string _selectedItem; @@ -20,7 +27,7 @@ public partial class FrequencyNavigationViewModel : IFrequencyNavigationViewMode set { if (_searchText == value) return; - + _searchText = value; OnPropertyChanged(); @@ -29,18 +36,48 @@ public partial class FrequencyNavigationViewModel : IFrequencyNavigationViewMode } 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() - { - _showWindow = _frequencyNavigationService.ShowWindow; - } + => _showWindow = _frequencyNavigationService.ShowWindow; private void UpdateFilteredMatches() { FilteredMatches = new List(_frequencyNavigationService.GetMatchingContainers(_searchText)); + if (FilteredMatches.Contains(SelectedItem)) return; + + SelectedItem = FilteredMatches.Count > 0 + ? FilteredMatches[0] + : null; } string IModalViewModel.Name => "FrequencyNavigation"; diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/appsettings.Development.json b/src/GuiApp/Avalonia/FileTime.GuiApp.App/appsettings.Development.json index bfd4587..cca6606 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/appsettings.Development.json +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/appsettings.Development.json @@ -1,7 +1,7 @@ { "Serilog": { "MinimumLevel": { - "Default": "Debug", + "Default": "Verbose", "Override": { "Microsoft": "Information", "System": "Warning" diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Logging/ToastMessageSink.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Logging/ToastMessageSink.cs index debc025..e89e6e0 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Logging/ToastMessageSink.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Logging/ToastMessageSink.cs @@ -18,6 +18,8 @@ public class ToastMessageSink : ILogEventSink if (logEvent.Level >= LogEventLevel.Error) { var message = logEvent.RenderMessage(); + if (logEvent.Exception is not null) + message += $" {logEvent.Exception.Message}"; dialogService.ShowToastMessage(message); } } diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Styles.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Styles.axaml index 8708c7a..54a4d7f 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Styles.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Styles.axaml @@ -40,6 +40,21 @@ + + + + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml index d6270cb..5021322 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml @@ -9,21 +9,28 @@ d:DesignWidth="800" x:CompileBindings="True" x:DataType="viewModels:IFrequencyNavigationViewModel" + Background="{DynamicResource ContainerBackgroundColor}" mc:Ignorable="d"> + + + - - + Classes="CommandPalette" + Items="{Binding FilteredMatches}" + SelectedItem="{Binding SelectedItem}"> + - - + + \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml.cs index 72701a0..7745e68 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml.cs @@ -26,5 +26,9 @@ public partial class FrequencyNavigation : UserControl { viewModel.Close(); } + else + { + viewModel.HandleKeyDown(e); + } } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml index 35360d9..d156d64 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml @@ -719,12 +719,13 @@ - + + +