refactor 'recent files'

rename RecentVault to RecentFile and move it to Models
create RecentFilesService to read and manipulate the recent file list
inject RecentFilesService into MainWindowViewModel
This commit is contained in:
2025-11-19 02:27:04 +03:00
parent bfd26b52fe
commit 85989057df
7 changed files with 147 additions and 60 deletions

View File

@@ -6,6 +6,7 @@ using System.Linq;
using Avalonia.Markup.Xaml;
using KeyKeeper.ViewModels;
using KeyKeeper.Views;
using KeyKeeper.Services;
namespace KeyKeeper;
@@ -25,7 +26,7 @@ public partial class App : Application
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
DataContext = new MainWindowViewModel(new RecentFilesService()),
};
}

View File

@@ -0,0 +1,25 @@
using System;
namespace KeyKeeper.Models;
public struct RecentFile
{
public string Path { get; set; } = string.Empty;
public DateTime LastOpened { get; set; }
public string DisplayPath => Path;
public RecentFile(string path, DateTime lastOpened)
{
Path = path;
LastOpened = lastOpened;
}
public RecentFile(string path): this(path, DateTime.Now)
{}
public override string ToString()
{
return DisplayPath;
}
}

View File

@@ -0,0 +1,16 @@
using System.Collections.ObjectModel;
using KeyKeeper.Models;
namespace KeyKeeper.Services;
public interface IRecentFilesService
{
// files are stored in reverse chronological order
ObservableCollection<RecentFile> RecentFiles { get; }
void Remember(string filename);
void Forget(string filename);
void ForgetAll();
// TODO load and store
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using KeyKeeper.Models;
namespace KeyKeeper.Services;
internal class RecentFilesService : IRecentFilesService
{
// files are stored in reverse chronological order
public ObservableCollection<RecentFile> RecentFiles { get; }
private readonly int maxEntries = 8;
public RecentFilesService()
{
RecentFiles = new ObservableCollection<RecentFile>();
}
public void Remember(string filename)
{
RemoveIfExists(filename);
RecentFiles.Insert(0, new RecentFile(filename));
if (RecentFiles.Count > maxEntries)
{
RecentFiles.RemoveAt(RecentFiles.Count - 1);
}
}
public void Forget(string filename)
{
RemoveIfExists(filename);
}
public void ForgetAll()
{
RecentFiles.Clear();
}
public void RemoveIfExists(string filename)
{
for (int i = 0; i < RecentFiles.Count; i++)
{
if (RecentFiles[i].Path == filename)
{
RecentFiles.RemoveAt(i);
break;
}
}
}
}

View File

@@ -3,11 +3,32 @@ using CommunityToolkit.Mvvm.Input;
using KeyKeeper.Views;
using Avalonia.Controls;
using System.Threading.Tasks;
using KeyKeeper.Services;
using System.Collections.ObjectModel;
using KeyKeeper.Models;
namespace KeyKeeper.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
public string Greeting { get; } = "Welcome to KeyKeeper!";
public ObservableCollection<RecentFile> RecentFiles => recentFilesService.RecentFiles;
private IRecentFilesService recentFilesService;
public MainWindowViewModel(IRecentFilesService recentFilesService)
{
this.recentFilesService = recentFilesService;
}
public void OpenVault(string filename)
{
recentFilesService.Remember(filename);
}
public void CreateVault(string filename)
{
recentFilesService.Remember(filename);
}
[RelayCommand]
private async Task OpenSettings()

View File

@@ -1,6 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:KeyKeeper.ViewModels"
xmlns:m="using:KeyKeeper.Models"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:KeyKeeper.Views"
@@ -15,15 +16,15 @@
</Design.DataContext>
<Grid>
<TextBlock Text="{Binding Greeting}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Margin="20">
<Button Content="Настройки"
Command="{Binding OpenSettingsCommand}"
Width="100"
Height="30"/>
</StackPanel>
<TextBlock Text="{Binding Greeting}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Margin="20">
<Button Content="Настройки"
Command="{Binding OpenSettingsCommand}"
Width="100"
Height="30"/>
</StackPanel>
<Grid Margin="20">
<Grid.RowDefinitions>
@@ -76,9 +77,10 @@
Height="120"
Background="Transparent"
BorderThickness="1"
BorderBrush="LightGray">
BorderBrush="LightGray"
ItemsSource="{Binding RecentFiles}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="local:RecentVault">
<DataTemplate x:DataType="m:RecentFile">
<TextBlock Text="{Binding DisplayPath}"
Margin="5"/>
</DataTemplate>

View File

@@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using KeyKeeper.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -15,68 +16,48 @@ namespace KeyKeeper.Views
public MainWindow()
{
InitializeComponent();
LoadRecentVaults();
}
private void LoadRecentVaults()
{
var recentFiles = new List<RecentVault>
{
new RecentVault { Path = "C:\\Users\\User\\Documents\\passwords.kdbx", LastOpened = DateTime.Now.AddDays(-1) },
new RecentVault { Path = "C:\\Users\\User\\Desktop\\work_passwords.kdbx", LastOpened = DateTime.Now.AddDays(-3) }
};
RecentVaultsList.ItemsSource = recentFiles;
}
private async void CreateNewVault_Click(object sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel == null) return;
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Создать новое хранилище паролей",
SuggestedFileName = "passwords.kdbx",
DefaultExtension = "kdbx",
SuggestedFileName = "passwords.kkp",
DefaultExtension = "kkp",
FileTypeChoices = new[]
{
new FilePickerFileType("KeyKeeper Database")
new FilePickerFileType("Хранилище KeyKeeper")
{
Patterns = new[] { "*.kdbx" }
Patterns = new[] { "*.kkp" }
}
}
});
if (file != null)
{
// Здесь будет логика создания нового хранилища
ShowMessage($"Создание нового хранилища: {file.Name}");
if (file.TryGetLocalPath() is string path)
{
ShowMessage($"Создание нового хранилища: {path}");
(DataContext as MainWindowViewModel)!.CreateVault(path);
}
}
}
private async void OpenExistingVault_Click(object sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel == null) return;
// Открываем диалог выбора файла
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Открыть хранилище паролей",
AllowMultiple = false,
FileTypeFilter = new[]
{
new FilePickerFileType("KeyKeeper Database")
new FilePickerFileType("Хранилище KeyKeeper")
{
Patterns = new[] { "*.kdbx", "*.kkdb" }
Patterns = new[] { "*.kkp" }
},
new FilePickerFileType("KeePass Database")
{
Patterns = new[] { "*.kdbx" }
},
new FilePickerFileType("All Files")
new FilePickerFileType("Все файлы")
{
Patterns = new[] { "*.*" }
}
@@ -86,7 +67,11 @@ namespace KeyKeeper.Views
if (files.Count > 0)
{
var file = files[0];
ShowMessage($"Открытие хранилища: {file.Name}");
if (file.TryGetLocalPath() is string path)
{
ShowMessage($"Открытие хранилища: {path}");
(DataContext as MainWindowViewModel)!.OpenVault(path);
}
}
}
@@ -103,17 +88,4 @@ namespace KeyKeeper.Views
messageBox.ShowDialog(this);
}
}
public class RecentVault
{
public string Path { get; set; } = string.Empty;
public DateTime LastOpened { get; set; }
public string DisplayPath => System.IO.Path.GetFileName(Path);
public override string ToString()
{
return DisplayPath;
}
}
}