diff --git a/src/KeyKeeper/App.axaml.cs b/src/KeyKeeper/App.axaml.cs index a55380a..e61b9f4 100644 --- a/src/KeyKeeper/App.axaml.cs +++ b/src/KeyKeeper/App.axaml.cs @@ -24,9 +24,11 @@ public partial class App : Application // Avoid duplicate validations from both Avalonia and the CommunityToolkit. // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins DisableAvaloniaDataAnnotationValidation(); - desktop.MainWindow = new MainWindow + var recentFilesService = new RecentFilesService(); + recentFilesService.Load(); + desktop.MainWindow = new MainWindow(recentFilesService) { - DataContext = new MainWindowViewModel(new RecentFilesService()), + DataContext = new MainWindowViewModel(recentFilesService), }; } diff --git a/src/KeyKeeper/Services/IRecentFilesService.cs b/src/KeyKeeper/Services/IRecentFilesService.cs index 8621da8..25894f0 100644 --- a/src/KeyKeeper/Services/IRecentFilesService.cs +++ b/src/KeyKeeper/Services/IRecentFilesService.cs @@ -7,10 +7,11 @@ public interface IRecentFilesService { // files are stored in reverse chronological order ObservableCollection RecentFiles { get; } - + + void Load(); + void Save(); + void Remember(string filename); void Forget(string filename); void ForgetAll(); - - // TODO load and store } \ No newline at end of file diff --git a/src/KeyKeeper/Services/RecentFilesService.cs b/src/KeyKeeper/Services/RecentFilesService.cs index 7ddc7e4..aa569c2 100644 --- a/src/KeyKeeper/Services/RecentFilesService.cs +++ b/src/KeyKeeper/Services/RecentFilesService.cs @@ -1,19 +1,69 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; +using System.Text.Json; using KeyKeeper.Models; namespace KeyKeeper.Services; internal class RecentFilesService : IRecentFilesService { + private const string RecentFilesFilename = "recent-files.json"; + private static readonly JsonSerializerOptions jsonOptions = new() { WriteIndented = true }; + // files are stored in reverse chronological order public ObservableCollection RecentFiles { get; } private readonly int maxEntries = 8; + private readonly string recentFilesPath; public RecentFilesService() { RecentFiles = new ObservableCollection(); + var appDataDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "KeyKeeper"); + recentFilesPath = Path.Combine(appDataDirectory, RecentFilesFilename); + } + + public void Load() + { + RecentFiles.Clear(); + + if (!File.Exists(recentFilesPath)) + { + return; + } + + try + { + var content = File.ReadAllText(recentFilesPath); + var loadedFiles = JsonSerializer.Deserialize>(content) ?? new List(); + + foreach (var recentFile in loadedFiles + .OrderByDescending(file => file.LastOpened) + .Take(maxEntries)) + { + RecentFiles.Add(recentFile); + } + } + catch + { + // ignore broken data and continue with empty recent files + } + } + + public void Save() + { + var directory = Path.GetDirectoryName(recentFilesPath); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + + var payload = JsonSerializer.Serialize(RecentFiles, jsonOptions); + File.WriteAllText(recentFilesPath, payload); } public void Remember(string filename) @@ -24,16 +74,19 @@ internal class RecentFilesService : IRecentFilesService { RecentFiles.RemoveAt(RecentFiles.Count - 1); } + Save(); } public void Forget(string filename) { RemoveIfExists(filename); + Save(); } public void ForgetAll() { RecentFiles.Clear(); + Save(); } public void RemoveIfExists(string filename) diff --git a/src/KeyKeeper/ViewModels/MainWindowViewModel.cs b/src/KeyKeeper/ViewModels/MainWindowViewModel.cs index 7903556..f967df0 100644 --- a/src/KeyKeeper/ViewModels/MainWindowViewModel.cs +++ b/src/KeyKeeper/ViewModels/MainWindowViewModel.cs @@ -20,16 +20,6 @@ public partial class MainWindowViewModel : ViewModelBase this.recentFilesService = recentFilesService; } - public void OpenVault(string filename) - { - recentFilesService.Remember(filename); - } - - public void CreateVault(string filename) - { - recentFilesService.Remember(filename); - } - [RelayCommand] private async Task OpenSettings() { diff --git a/src/KeyKeeper/Views/MainWindow.axaml b/src/KeyKeeper/Views/MainWindow.axaml index 5f2cd54..86a0554 100644 --- a/src/KeyKeeper/Views/MainWindow.axaml +++ b/src/KeyKeeper/Views/MainWindow.axaml @@ -78,9 +78,14 @@ ItemsSource="{Binding RecentFiles}"> - + + + @@ -93,7 +98,5 @@ - - diff --git a/src/KeyKeeper/Views/MainWindow.axaml.cs b/src/KeyKeeper/Views/MainWindow.axaml.cs index 3e0a731..aea5e39 100644 --- a/src/KeyKeeper/Views/MainWindow.axaml.cs +++ b/src/KeyKeeper/Views/MainWindow.axaml.cs @@ -3,8 +3,10 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Platform.Storage; +using KeyKeeper.Models; using KeyKeeper.PasswordStore; using KeyKeeper.PasswordStore.Crypto; +using KeyKeeper.Services; using KeyKeeper.ViewModels; using System; using System.Collections.Generic; @@ -15,8 +17,11 @@ namespace KeyKeeper.Views { public partial class MainWindow : Window { - public MainWindow() + private IRecentFilesService recentFilesService; + + public MainWindow(IRecentFilesService recentFilesService) { + this.recentFilesService = recentFilesService; InitializeComponent(); this.MinWidth = 550; this.MinHeight = 350; @@ -42,6 +47,9 @@ namespace KeyKeeper.Views Key = compositeKey, LockTimeoutSeconds = 800 }); + + recentFilesService.Remember(path); + IPassStore passStore = passStoreAccessor; OpenRepositoryWindow(passStore); } @@ -71,11 +79,21 @@ namespace KeyKeeper.Views var file = files[0]; if (file.TryGetLocalPath() is string path) { + recentFilesService.Remember(path); OpenRepositoryWindow(new PassStoreFileAccessor(path, false, null)); } } } + private void RecentVaultsListItem_DoubleTapped(object sender, RoutedEventArgs e) + { + if (sender is Control c && c.DataContext is RecentFile recentFile) + { + recentFilesService.Remember(recentFile.Path); + OpenRepositoryWindow(new PassStoreFileAccessor(recentFile.Path, false, null)); + } + } + private void OpenRepositoryWindow(IPassStore passStore) { var repositoryWindow = new RepositoryWindow(new RepositoryWindowViewModel(passStore))