merge branch 'feature/recent-files'

This commit is contained in:
2026-04-26 21:02:20 +03:00
6 changed files with 88 additions and 21 deletions

View File

@@ -24,9 +24,11 @@ public partial class App : Application
// Avoid duplicate validations from both Avalonia and the CommunityToolkit. // Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation(); 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),
}; };
} }

View File

@@ -7,10 +7,11 @@ public interface IRecentFilesService
{ {
// files are stored in reverse chronological order // files are stored in reverse chronological order
ObservableCollection<RecentFile> RecentFiles { get; } ObservableCollection<RecentFile> RecentFiles { get; }
void Load();
void Save();
void Remember(string filename); void Remember(string filename);
void Forget(string filename); void Forget(string filename);
void ForgetAll(); void ForgetAll();
// TODO load and store
} }

View File

@@ -1,19 +1,69 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json;
using KeyKeeper.Models; using KeyKeeper.Models;
namespace KeyKeeper.Services; namespace KeyKeeper.Services;
internal class RecentFilesService : IRecentFilesService 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 // files are stored in reverse chronological order
public ObservableCollection<RecentFile> RecentFiles { get; } public ObservableCollection<RecentFile> RecentFiles { get; }
private readonly int maxEntries = 8; private readonly int maxEntries = 8;
private readonly string recentFilesPath;
public RecentFilesService() public RecentFilesService()
{ {
RecentFiles = new ObservableCollection<RecentFile>(); RecentFiles = new ObservableCollection<RecentFile>();
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<List<RecentFile>>(content) ?? new List<RecentFile>();
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) public void Remember(string filename)
@@ -24,16 +74,19 @@ internal class RecentFilesService : IRecentFilesService
{ {
RecentFiles.RemoveAt(RecentFiles.Count - 1); RecentFiles.RemoveAt(RecentFiles.Count - 1);
} }
Save();
} }
public void Forget(string filename) public void Forget(string filename)
{ {
RemoveIfExists(filename); RemoveIfExists(filename);
Save();
} }
public void ForgetAll() public void ForgetAll()
{ {
RecentFiles.Clear(); RecentFiles.Clear();
Save();
} }
public void RemoveIfExists(string filename) public void RemoveIfExists(string filename)

View File

@@ -20,16 +20,6 @@ public partial class MainWindowViewModel : ViewModelBase
this.recentFilesService = recentFilesService; this.recentFilesService = recentFilesService;
} }
public void OpenVault(string filename)
{
recentFilesService.Remember(filename);
}
public void CreateVault(string filename)
{
recentFilesService.Remember(filename);
}
[RelayCommand] [RelayCommand]
private async Task OpenSettings() private async Task OpenSettings()
{ {

View File

@@ -78,9 +78,14 @@
ItemsSource="{Binding RecentFiles}"> ItemsSource="{Binding RecentFiles}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="m:RecentFile"> <DataTemplate x:DataType="m:RecentFile">
<TextBlock Text="{Binding DisplayPath}" <Border Background="Transparent"
Foreground="#000" Cursor="Hand"
Margin="5"/> DoubleTapped="RecentVaultsListItem_DoubleTapped"
Padding="10">
<TextBlock Text="{Binding DisplayPath}"
Foreground="#000"
Margin="5"/>
</Border>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
<ListBox.Styles> <ListBox.Styles>
@@ -93,7 +98,5 @@
</Border> </Border>
</Grid> </Grid>
</Grid> </Grid>
</Grid> </Grid>
</Window> </Window>

View File

@@ -3,8 +3,10 @@ using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using KeyKeeper.Models;
using KeyKeeper.PasswordStore; using KeyKeeper.PasswordStore;
using KeyKeeper.PasswordStore.Crypto; using KeyKeeper.PasswordStore.Crypto;
using KeyKeeper.Services;
using KeyKeeper.ViewModels; using KeyKeeper.ViewModels;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -15,8 +17,11 @@ namespace KeyKeeper.Views
{ {
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
public MainWindow() private IRecentFilesService recentFilesService;
public MainWindow(IRecentFilesService recentFilesService)
{ {
this.recentFilesService = recentFilesService;
InitializeComponent(); InitializeComponent();
this.MinWidth = 550; this.MinWidth = 550;
this.MinHeight = 350; this.MinHeight = 350;
@@ -42,6 +47,9 @@ namespace KeyKeeper.Views
Key = compositeKey, Key = compositeKey,
LockTimeoutSeconds = 800 LockTimeoutSeconds = 800
}); });
recentFilesService.Remember(path);
IPassStore passStore = passStoreAccessor; IPassStore passStore = passStoreAccessor;
OpenRepositoryWindow(passStore); OpenRepositoryWindow(passStore);
} }
@@ -71,11 +79,21 @@ namespace KeyKeeper.Views
var file = files[0]; var file = files[0];
if (file.TryGetLocalPath() is string path) if (file.TryGetLocalPath() is string path)
{ {
recentFilesService.Remember(path);
OpenRepositoryWindow(new PassStoreFileAccessor(path, false, null)); 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) private void OpenRepositoryWindow(IPassStore passStore)
{ {
var repositoryWindow = new RepositoryWindow(new RepositoryWindowViewModel(passStore)) var repositoryWindow = new RepositoryWindow(new RepositoryWindowViewModel(passStore))