merge branch 'feature/configurable-lock-timer'

This commit is contained in:
2026-05-08 16:47:18 +03:00
4 changed files with 141 additions and 28 deletions

View File

@@ -9,6 +9,7 @@ public static class AppSettings
private static readonly string FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "KeyKeeper", "settings.json"); private static readonly string FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "KeyKeeper", "settings.json");
public static bool ExitOnRepositoryClose { get; set; } = false; public static bool ExitOnRepositoryClose { get; set; } = false;
public static int LockTimerMinutes { get; set; } = 5;
// Сохранение в файл // Сохранение в файл
public static void Save() public static void Save()
@@ -17,7 +18,7 @@ public static class AppSettings
if (!string.IsNullOrEmpty(directory)) if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
var data = new { ExitOnRepositoryClose }; var data = new { ExitOnRepositoryClose, LockTimerMinutes };
string json = JsonSerializer.Serialize(data); string json = JsonSerializer.Serialize(data);
File.WriteAllText(FilePath, json); File.WriteAllText(FilePath, json);
} }
@@ -34,6 +35,7 @@ public static class AppSettings
if (data != null) if (data != null)
{ {
ExitOnRepositoryClose = data.ExitOnRepositoryClose; ExitOnRepositoryClose = data.ExitOnRepositoryClose;
LockTimerMinutes = data.LockTimerMinutes ?? 5;
} }
} }
catch { /* Если файл поврежден, просто используем значения по умолчанию */ } catch { /* Если файл поврежден, просто используем значения по умолчанию */ }
@@ -43,5 +45,6 @@ public static class AppSettings
private class SettingsData private class SettingsData
{ {
public bool ExitOnRepositoryClose { get; set; } public bool ExitOnRepositoryClose { get; set; }
public int? LockTimerMinutes { get; set; }
} }
} }

View File

@@ -1,4 +1,5 @@
using Avalonia; using System;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
@@ -49,14 +50,60 @@ public class SettingsWindow : Window
exitOnCloseCheckBox.IsCheckedChanged += (s, e) => exitOnCloseCheckBox.IsCheckedChanged += (s, e) =>
{ {
AppSettings.ExitOnRepositoryClose = exitOnCloseCheckBox.IsChecked ?? false; AppSettings.ExitOnRepositoryClose = exitOnCloseCheckBox.IsChecked ?? false;
AppSettings.Save();
}; };
// Настройка таймера блокировки
var lockTimerDurationRow = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 12,
};
var lockTimerDurationLabel1 = new TextBlock
{
Text = "Lock the vault after",
VerticalAlignment = VerticalAlignment.Center,
};
var lockTimerDurationInput = new NumericUpDown
{
Value = AppSettings.LockTimerMinutes,
Increment = 1,
Minimum = 1,
Maximum = 90,
ClipValueToMinMax = true,
Width = 120,
};
var lockTimerDurationLabel2 = new TextBlock
{
Text = "minutes of inactivity",
VerticalAlignment = VerticalAlignment.Center,
};
lockTimerDurationInput.ValueChanged += (_, _) =>
{
Console.WriteLine($"Set timer to {lockTimerDurationInput.Value} minutes");
AppSettings.LockTimerMinutes = (int)(lockTimerDurationInput.Value ?? 5m);
};
lockTimerDurationRow.Children.Add(lockTimerDurationLabel1);
lockTimerDurationRow.Children.Add(lockTimerDurationInput);
lockTimerDurationRow.Children.Add(lockTimerDurationLabel2);
// Добавляем элементы в стек // Добавляем элементы в стек
mainStack.Children.Add(titleText); mainStack.Children.Add(titleText);
mainStack.Children.Add(exitOnCloseCheckBox); mainStack.Children.Add(exitOnCloseCheckBox);
mainStack.Children.Add(lockTimerDurationRow);
// Назначаем стек основным контентом окна // Назначаем стек основным контентом окна
this.Content = mainStack; this.Content = mainStack;
} }
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
Console.WriteLine("Saving application settings");
AppSettings.Save();
}
} }

View File

@@ -8,8 +8,6 @@ namespace KeyKeeper.ViewModels;
public partial class RepositoryWindowViewModel : ViewModelBase public partial class RepositoryWindowViewModel : ViewModelBase
{ {
private static readonly TimeSpan LockTimeout = TimeSpan.FromMinutes(5);
private object currentPage; private object currentPage;
private IPassStore passStore; private IPassStore passStore;
private DispatcherTimer? _lockTimer; private DispatcherTimer? _lockTimer;
@@ -94,7 +92,7 @@ public partial class RepositoryWindowViewModel : ViewModelBase
private void OnLockTimerTick(object? sender, EventArgs e) private void OnLockTimerTick(object? sender, EventArgs e)
{ {
var elapsed = DateTime.UtcNow - _timerStart; var elapsed = DateTime.UtcNow - _timerStart;
var remaining = LockTimeout - elapsed; var remaining = TimeSpan.FromMinutes(AppSettings.LockTimerMinutes) - elapsed;
if (remaining <= TimeSpan.Zero) if (remaining <= TimeSpan.Zero)
{ {
@@ -109,7 +107,7 @@ public partial class RepositoryWindowViewModel : ViewModelBase
private void UpdateTimerDisplay(TimeSpan? remaining = null) private void UpdateTimerDisplay(TimeSpan? remaining = null)
{ {
var r = remaining ?? LockTimeout; var r = remaining ?? TimeSpan.FromMinutes(AppSettings.LockTimerMinutes);
LockTimerDisplay = $"{r:mm\\:ss}"; LockTimerDisplay = $"{r:mm\\:ss}";
} }
} }

View File

@@ -10,6 +10,7 @@ using KeyKeeper.Services;
using KeyKeeper.ViewModels; using KeyKeeper.ViewModels;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -39,19 +40,31 @@ namespace KeyKeeper.Views
var path = createVaultDialog.FilePath; var path = createVaultDialog.FilePath;
var password = createVaultDialog.Password; var password = createVaultDialog.Password;
var compositeKey = new CompositeKey(password, null); var compositeKey = new CompositeKey(password, null);
var passStoreAccessor = new PassStoreFileAccessor(
filename: path,
create: true,
createOptions: new StoreCreationOptions()
{
Key = compositeKey,
LockTimeoutSeconds = 800
});
recentFilesService.Remember(path); try
{
var passStoreAccessor = new PassStoreFileAccessor(
filename: path,
create: true,
createOptions: new StoreCreationOptions()
{
Key = compositeKey,
LockTimeoutSeconds = 800
});
IPassStore passStore = passStoreAccessor; recentFilesService.Remember(path);
OpenRepositoryWindow(passStore);
IPassStore passStore = passStoreAccessor;
OpenRepositoryWindow(passStore);
} catch (IOException exception)
{
Console.WriteLine($"I/O error when creating \"{path}\": {exception}");
await new ErrorDialog("Cannot create the password store", "File error").ShowDialog(this);
} catch (Exception exception)
{
Console.WriteLine($"Unknown error when creating \"{path}\": {exception}");
await new ErrorDialog("Cannot create the password store", "Unknown error").ShowDialog(this);
}
} }
} }
@@ -61,17 +74,17 @@ namespace KeyKeeper.Views
{ {
Title = "Открыть хранилище паролей", Title = "Открыть хранилище паролей",
AllowMultiple = false, AllowMultiple = false,
FileTypeFilter = new[] FileTypeFilter =
{ [
new FilePickerFileType("KeyKeeper files") new FilePickerFileType("KeyKeeper files")
{ {
Patterns = new[] { "*.kkp" } Patterns = ["*.kkp"]
}, },
new FilePickerFileType("All files") new FilePickerFileType("All files")
{ {
Patterns = new[] { "*.*" } Patterns = ["*.*"]
} }
} ]
}); });
if (files.Count > 0) if (files.Count > 0)
@@ -80,17 +93,69 @@ namespace KeyKeeper.Views
if (file.TryGetLocalPath() is string path) if (file.TryGetLocalPath() is string path)
{ {
recentFilesService.Remember(path); recentFilesService.Remember(path);
OpenRepositoryWindow(new PassStoreFileAccessor(path, false, null)); IPassStore? passStore;
try
{
passStore = new PassStoreFileAccessor(path, false, null);
} catch (PassStoreFileException exc)
{
await new ErrorDialog($"This password store file has a problem: {exc.Message}", "File format error").ShowDialog(this);
Console.WriteLine($"Format error when opening \"{path}\": {exc}");
recentFilesService.Forget(path);
return;
} catch (FileNotFoundException)
{
await new ErrorDialog("This password store no longer exists", "File error").ShowDialog(this);
recentFilesService.Forget(path);
return;
} catch (IOException exc)
{
Console.WriteLine($"I/O error when opening \"{path}\": {exc}");
await new ErrorDialog("Cannot open this password store", "File error").ShowDialog(this);
return;
} catch (Exception exc)
{
Console.WriteLine($"Unknown error when opening \"{path}\": {exc}");
await new ErrorDialog("Cannot open this password store", "File error").ShowDialog(this);
return;
}
OpenRepositoryWindow(passStore);
} }
} }
} }
private void RecentVaultsListItem_DoubleTapped(object sender, RoutedEventArgs e) private async void RecentVaultsListItem_DoubleTapped(object sender, RoutedEventArgs e)
{ {
if (sender is Control c && c.DataContext is RecentFile recentFile) if (sender is Control c && c.DataContext is RecentFile recentFile)
{ {
recentFilesService.Remember(recentFile.Path); recentFilesService.Remember(recentFile.Path);
OpenRepositoryWindow(new PassStoreFileAccessor(recentFile.Path, false, null)); IPassStore? passStore;
try
{
passStore = new PassStoreFileAccessor(recentFile.Path, false, null);
} catch (PassStoreFileException exc)
{
await new ErrorDialog($"This password store file has a problem: {exc.Message}", "File format error").ShowDialog(this);
Console.WriteLine($"Format error when opening \"{recentFile.Path}\" from recents: {exc}");
recentFilesService.Forget(recentFile.Path);
return;
} catch (FileNotFoundException)
{
await new ErrorDialog("This password store no longer exists", "File error").ShowDialog(this);
recentFilesService.Forget(recentFile.Path);
return;
} catch (IOException exc)
{
Console.WriteLine($"I/O error when opening \"{recentFile.Path}\" from recents: {exc}");
await new ErrorDialog("Cannot open this password store", "File error").ShowDialog(this);
return;
} catch (Exception exc)
{
Console.WriteLine($"Unknown error when opening \"{recentFile.Path}\" from recents: {exc}");
await new ErrorDialog("Cannot open this password store", "File error").ShowDialog(this);
return;
}
OpenRepositoryWindow(passStore);
} }
} }
@@ -104,11 +169,11 @@ namespace KeyKeeper.Views
{ {
if (AppSettings.ExitOnRepositoryClose) if (AppSettings.ExitOnRepositoryClose)
{ {
this.Close(); Close();
} }
else else
{ {
this.Show(); Show();
} }
}; };
repositoryWindow.Show(); repositoryWindow.Show();