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");
public static bool ExitOnRepositoryClose { get; set; } = false;
public static int LockTimerMinutes { get; set; } = 5;
// Сохранение в файл
public static void Save()
@@ -17,7 +18,7 @@ public static class AppSettings
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);
var data = new { ExitOnRepositoryClose };
var data = new { ExitOnRepositoryClose, LockTimerMinutes };
string json = JsonSerializer.Serialize(data);
File.WriteAllText(FilePath, json);
}
@@ -34,6 +35,7 @@ public static class AppSettings
if (data != null)
{
ExitOnRepositoryClose = data.ExitOnRepositoryClose;
LockTimerMinutes = data.LockTimerMinutes ?? 5;
}
}
catch { /* Если файл поврежден, просто используем значения по умолчанию */ }
@@ -43,5 +45,6 @@ public static class AppSettings
private class SettingsData
{
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.Layout;
using Avalonia.Media;
@@ -49,14 +50,60 @@ public class SettingsWindow : Window
exitOnCloseCheckBox.IsCheckedChanged += (s, e) =>
{
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(exitOnCloseCheckBox);
mainStack.Children.Add(lockTimerDurationRow);
// Назначаем стек основным контентом окна
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
{
private static readonly TimeSpan LockTimeout = TimeSpan.FromMinutes(5);
private object currentPage;
private IPassStore passStore;
private DispatcherTimer? _lockTimer;
@@ -94,7 +92,7 @@ public partial class RepositoryWindowViewModel : ViewModelBase
private void OnLockTimerTick(object? sender, EventArgs e)
{
var elapsed = DateTime.UtcNow - _timerStart;
var remaining = LockTimeout - elapsed;
var remaining = TimeSpan.FromMinutes(AppSettings.LockTimerMinutes) - elapsed;
if (remaining <= TimeSpan.Zero)
{
@@ -109,7 +107,7 @@ public partial class RepositoryWindowViewModel : ViewModelBase
private void UpdateTimerDisplay(TimeSpan? remaining = null)
{
var r = remaining ?? LockTimeout;
var r = remaining ?? TimeSpan.FromMinutes(AppSettings.LockTimerMinutes);
LockTimerDisplay = $"{r:mm\\:ss}";
}
}

View File

@@ -10,6 +10,7 @@ using KeyKeeper.Services;
using KeyKeeper.ViewModels;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -39,19 +40,31 @@ namespace KeyKeeper.Views
var path = createVaultDialog.FilePath;
var password = createVaultDialog.Password;
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;
OpenRepositoryWindow(passStore);
recentFilesService.Remember(path);
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 = "Открыть хранилище паролей",
AllowMultiple = false,
FileTypeFilter = new[]
{
FileTypeFilter =
[
new FilePickerFileType("KeyKeeper files")
{
Patterns = new[] { "*.kkp" }
Patterns = ["*.kkp"]
},
new FilePickerFileType("All files")
{
Patterns = new[] { "*.*" }
Patterns = ["*.*"]
}
}
]
});
if (files.Count > 0)
@@ -80,17 +93,69 @@ namespace KeyKeeper.Views
if (file.TryGetLocalPath() is string 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)
{
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)
{
this.Close();
Close();
}
else
{
this.Show();
Show();
}
};
repositoryWindow.Show();