mirror of
https://github.com/KeyKeeperApp/KeyKeeper.git
synced 2026-05-17 13:56:31 +03:00
merge branch 'feature/recent-files'
This commit is contained in:
@@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ 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
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user