merge branch 'feature/session-timeout'

This commit is contained in:
2026-03-24 23:01:49 +03:00
3 changed files with 174 additions and 61 deletions

View File

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Avalonia.Threading;
using KeyKeeper.PasswordStore;
using static KeyKeeper.PasswordStore.FileFormatConstants;
@@ -7,8 +8,13 @@ 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;
private DateTime _timerStart;
private string _lockTimerDisplay = string.Empty;
public Func<string, Task> ShowErrorPopup;
@@ -18,6 +24,12 @@ public partial class RepositoryWindowViewModel : ViewModelBase
set { currentPage = value; OnPropertyChanged(nameof(CurrentPage)); }
}
public string LockTimerDisplay
{
get => _lockTimerDisplay;
private set { _lockTimerDisplay = value; OnPropertyChanged(nameof(LockTimerDisplay)); }
}
public RepositoryWindowViewModel(IPassStore store)
{
passStore = store;
@@ -32,15 +44,72 @@ public partial class RepositoryWindowViewModel : ViewModelBase
SwitchToLocked();
}
/// <summary>
/// Сбрасывает таймер блокировки (вызывается при любой активности пользователя).
/// </summary>
public void ResetLockTimer()
{
if (_lockTimer != null && _lockTimer.IsEnabled)
_timerStart = DateTime.UtcNow;
}
private void SwitchToUnlocked()
{
var directory = passStore.GetGroupByType(GROUP_TYPE_DEFAULT)
?? passStore.GetRootDirectory();
CurrentPage = new UnlockedRepositoryViewModel(passStore, directory);
StartLockTimer();
}
private void SwitchToLocked()
{
StopLockTimer();
CurrentPage = new LockedRepositoryViewModel(passStore, this);
}
}
public void StartLockTimer()
{
StopLockTimer();
_timerStart = DateTime.UtcNow;
_lockTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_lockTimer.Tick += OnLockTimerTick;
_lockTimer.Start();
UpdateTimerDisplay();
}
public void StopLockTimer()
{
if (_lockTimer != null)
{
_lockTimer.Tick -= OnLockTimerTick;
_lockTimer.Stop();
_lockTimer = null;
}
LockTimerDisplay = string.Empty;
}
private void OnLockTimerTick(object? sender, EventArgs e)
{
var elapsed = DateTime.UtcNow - _timerStart;
var remaining = LockTimeout - elapsed;
if (remaining <= TimeSpan.Zero)
{
StopLockTimer();
passStore.Lock();
UpdateLockStatus();
return;
}
UpdateTimerDisplay(remaining);
}
private void UpdateTimerDisplay(TimeSpan? remaining = null)
{
var r = remaining ?? LockTimeout;
LockTimerDisplay = $"{r:mm\\:ss}";
}
}

View File

@@ -4,7 +4,7 @@
xmlns:i="using:Avalonia.Interactivity"
xmlns:kkp="using:KeyKeeper.Views"
x:Class="KeyKeeper.Views.RepositoryWindow"
Closing="RepositoryWindow_Closing"
Closing="RepositoryWindow_Closing"
Title="KeyKeeper - Password store"
CanResize="False"
Width="800"
@@ -12,36 +12,59 @@
Background="White"
x:DataType="vm:RepositoryWindowViewModel">
<Window.DataTemplates>
<DataTemplate DataType="{x:Type vm:UnlockedRepositoryViewModel}">
<Grid>
<!-- Синий левый край -->
<Border Width="200"
<Window.DataTemplates>
<DataTemplate DataType="{x:Type vm:UnlockedRepositoryViewModel}">
<Grid>
<!-- Синий левый край -->
<Border Width="200"
Background="#2328C4"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"/>
<StackPanel Margin="20" HorizontalAlignment="Left">
<!-- Надпись KeyKeeper -->
<TextBlock Text="KeyKeeper"
<StackPanel Margin="20" HorizontalAlignment="Left">
<!-- Надпись KeyKeeper -->
<TextBlock Text="KeyKeeper"
FontSize="32"
FontWeight="Bold"
HorizontalAlignment="Left"
Margin="0,0,0,20"/>
<!-- Таймер блокировки под надписью KeyKeeper -->
<Border Background="#CC000000"
CornerRadius="6"
Padding="8,4"
HorizontalAlignment="Left"
Margin="0,0,0,12"
IsVisible="{Binding $parent[Window].DataContext.LockTimerDisplay,
Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<!-- Иконка замка (Material Design lock outline) -->
<Path Fill="White"
VerticalAlignment="Center"
Width="13" Height="13"
Stretch="Uniform"
Data="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
<TextBlock Text="{Binding $parent[Window].DataContext.LockTimerDisplay}"
Foreground="White"
FontSize="13"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<!-- Рамочка -->
<!-- <Border BorderBrush="Gray"
<!-- <Border BorderBrush="Gray"
BorderThickness="1"
CornerRadius="5"
Padding="20"
Background="#F5F5F5"
HorizontalAlignment="Left">
<StackPanel HorizontalAlignment="Left">
<Button Content="All Passwords"
<StackPanel HorizontalAlignment="Left">
<Button Content="All Passwords"
Width="120"
Height="30"
HorizontalAlignment="Left"/>
</StackPanel>
</StackPanel>
</Border> -->
<!-- Save Passwords -->
@@ -52,93 +75,93 @@
HorizontalAlignment="Left"
Margin="0,20,0,0"/>
<!-- New Entry -->
<Button Content="New Entry"
<!-- New Entry -->
<Button Content="New Entry"
Classes="accentSidebarButton"
Click="AddEntryButton_Click"
Height="30"
HorizontalAlignment="Left"
Margin="0,20,0,0"/>
<!-- Edit Selected Entry -->
<Button Content="Edit Selected Entry"
<!-- Edit Selected Entry -->
<Button Content="Edit Selected Entry"
Classes="accentSidebarButton"
Click="EditEntryButton_Click"
Height="30"
HorizontalAlignment="Left"
Margin="0,20,0,0"/>
</StackPanel>
</StackPanel>
<!-- ListBox с паролями -->
<ListBox x:Name="PasswordsListBox"
<!-- ListBox с паролями -->
<ListBox x:Name="PasswordsListBox"
Width="580"
Margin="210 10 10 10"
ItemsSource="{Binding Passwords}"
Background="Transparent"
SelectionMode="Single">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="Transparent" DoubleTapped="Entry_DoubleTapped">
<StackPanel Width="100"
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="Transparent" DoubleTapped="Entry_DoubleTapped">
<StackPanel Width="100"
Margin="10"
HorizontalAlignment="Center">
<Svg Path="{Binding IconPath}" Width="48" Height="48"/>
<TextBlock Text="{Binding Name}"
<Svg Path="{Binding IconPath}" Width="48" Height="48"/>
<TextBlock Text="{Binding Name}"
HorizontalAlignment="Center"
Foreground="Black" />
<TextBlock Text="{Binding Username.Value}"
<TextBlock Text="{Binding Username.Value}"
Foreground="#666"
HorizontalAlignment="Center" />
</StackPanel>
<Border.ContextMenu>
<ContextMenu>
<MenuItem Name="entryCtxMenuCopyUsername" Header="Copy username" Click="EntryContextMenuItem_Click"/>
<MenuItem Name="entryCtxMenuCopyPassword" Header="Copy password" Click="EntryContextMenuItem_Click"/>
<!-- Новый пункт меню "Edit" -->
<MenuItem Name="entryCtxMenuEdit" Header="Edit" Click="EntryContextMenuItem_Click"/>
<MenuItem Name="entryCtxMenuDelete" Header="Delete" Click="EntryContextMenuItem_Click"/>
</ContextMenu>
</Border.ContextMenu>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
<Border.ContextMenu>
<ContextMenu>
<MenuItem Name="entryCtxMenuCopyUsername" Header="Copy username" Click="EntryContextMenuItem_Click"/>
<MenuItem Name="entryCtxMenuCopyPassword" Header="Copy password" Click="EntryContextMenuItem_Click"/>
<!-- Новый пункт меню "Edit" -->
<MenuItem Name="entryCtxMenuEdit" Header="Edit" Click="EntryContextMenuItem_Click"/>
<MenuItem Name="entryCtxMenuDelete" Header="Delete" Click="EntryContextMenuItem_Click"/>
</ContextMenu>
</Border.ContextMenu>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<kkp:ToastNotificationHost x:Name="NotificationHost"
<kkp:ToastNotificationHost x:Name="NotificationHost"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="20"
Duration="0:0:2" />
</Grid>
</DataTemplate>
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:LockedRepositoryViewModel}">
<StackPanel Margin="20"
<DataTemplate DataType="{x:Type vm:LockedRepositoryViewModel}">
<StackPanel Margin="20"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="10">
<TextBlock Text="Enter credentials to unlock"
<TextBlock Text="Enter credentials to unlock"
Foreground="#2328C4"
FontSize="32" />
<TextBox x:Name="UnlockPasswordEdit"
<TextBox x:Name="UnlockPasswordEdit"
Text="{Binding UnlockPassword, Mode=TwoWay}"
PasswordChar="*"
Width="450" />
<Button x:Name="UnlockButton"
<Button x:Name="UnlockButton"
Command="{Binding TryUnlock}"
HorizontalAlignment="Center"
Content="Unlock!" />
</StackPanel>
</DataTemplate>
</Window.DataTemplates>
</StackPanel>
</DataTemplate>
</Window.DataTemplates>
<ContentControl Content="{Binding CurrentPage}"/>
</Window>
<ContentControl Content="{Binding CurrentPage}"/>
</Window>

View File

@@ -28,6 +28,15 @@ public partial class RepositoryWindow : Window
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
AddHandler(PointerMovedEvent, OnUserActivity, RoutingStrategies.Tunnel);
AddHandler(PointerPressedEvent, OnUserActivity, RoutingStrategies.Tunnel);
AddHandler(KeyDownEvent, OnUserActivity, RoutingStrategies.Tunnel);
}
private void OnUserActivity(object? sender, RoutedEventArgs e)
{
if (DataContext is RepositoryWindowViewModel vm)
vm.ResetLockTimer();
}
private async void RepositoryWindow_Closing(object? sender, WindowClosingEventArgs e)
@@ -77,8 +86,13 @@ public partial class RepositoryWindow : Window
if (DataContext is RepositoryWindowViewModel vm_ && vm_.CurrentPage is UnlockedRepositoryViewModel vm)
{
EntryEditWindow dialog = new();
vm_.StopLockTimer();
await dialog.ShowDialog(this);
vm_.StartLockTimer();
if (dialog.EditedEntry != null)
vm.AddEntry(dialog.EditedEntry);
}
@@ -104,8 +118,13 @@ public partial class RepositoryWindow : Window
EntryEditWindow dialog = new();
dialog.SetEntry(selectedEntry);
vm_.StopLockTimer();
await dialog.ShowDialog(this);
vm_.StartLockTimer();
if (dialog.EditedEntry != null)
{
vm.UpdateEntry(dialog.EditedEntry);
@@ -151,7 +170,9 @@ public partial class RepositoryWindow : Window
{
EntryEditWindow dialog = new();
dialog.SetEntry(pwd);
vm.StopLockTimer();
await dialog.ShowDialog(this);
vm.StartLockTimer();
if (dialog.EditedEntry != null)
{
pageVm.UpdateEntry(dialog.EditedEntry);
@@ -202,4 +223,4 @@ public static class VisualTreeExtensions
return null;
}
}
}