mirror of
https://github.com/KeyKeeperApp/KeyKeeper.git
synced 2026-04-18 02:26:28 +03:00
merge branch 'master' into feature/session-timeout
This commit is contained in:
@@ -7,6 +7,7 @@ public interface IPassStore
|
||||
bool Locked { get; }
|
||||
|
||||
IPassStoreDirectory GetRootDirectory();
|
||||
IPassStoreDirectory? GetGroupByType(byte groupType);
|
||||
int GetTotalEntryCount();
|
||||
void Unlock(CompositeKey key);
|
||||
void Lock();
|
||||
|
||||
@@ -6,4 +6,5 @@ namespace KeyKeeper.PasswordStore;
|
||||
public interface IPassStoreDirectory : IEnumerable<PassStoreEntry>
|
||||
{
|
||||
bool DeleteEntry(Guid id);
|
||||
void AddEntry(PassStoreEntry entry);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,12 @@ public class PassStoreEntryGroup : PassStoreEntry, IPassStoreDirectory
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEntry(PassStoreEntry entry)
|
||||
{
|
||||
entry.Parent = this;
|
||||
ChildEntries.Add(entry);
|
||||
}
|
||||
|
||||
public bool DeleteEntry(Guid id)
|
||||
{
|
||||
if (ChildEntries == null)
|
||||
|
||||
@@ -49,6 +49,15 @@ public class PassStoreFileAccessor : IPassStore
|
||||
return (IPassStoreDirectory)root!;
|
||||
}
|
||||
|
||||
public IPassStoreDirectory? GetGroupByType(byte groupType)
|
||||
{
|
||||
if (Locked)
|
||||
throw new InvalidOperationException();
|
||||
return (root as PassStoreEntryGroup)?.ChildEntries
|
||||
.OfType<PassStoreEntryGroup>()
|
||||
.FirstOrDefault(g => g.GroupType == groupType);
|
||||
}
|
||||
|
||||
public int GetTotalEntryCount()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
@@ -252,15 +261,24 @@ public class PassStoreFileAccessor : IPassStore
|
||||
|
||||
private PassStoreEntry WriteInitialStoreTree(OuterEncryptionWriter w)
|
||||
{
|
||||
PassStoreEntry root =
|
||||
new PassStoreEntryGroup(
|
||||
Guid.NewGuid(),
|
||||
DateTime.UtcNow,
|
||||
DateTime.UtcNow,
|
||||
Guid.Empty,
|
||||
"",
|
||||
GROUP_TYPE_ROOT
|
||||
);
|
||||
PassStoreEntryGroup defaultGroup = new(
|
||||
Guid.NewGuid(),
|
||||
DateTime.UtcNow,
|
||||
DateTime.UtcNow,
|
||||
Guid.Empty,
|
||||
"",
|
||||
GROUP_TYPE_DEFAULT
|
||||
);
|
||||
PassStoreEntryGroup root = new(
|
||||
Guid.NewGuid(),
|
||||
DateTime.UtcNow,
|
||||
DateTime.UtcNow,
|
||||
Guid.Empty,
|
||||
"",
|
||||
GROUP_TYPE_ROOT,
|
||||
[defaultGroup]
|
||||
);
|
||||
defaultGroup.Parent = root;
|
||||
root.WriteToStream(w);
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
using KeyKeeper.PasswordStore;
|
||||
using static KeyKeeper.PasswordStore.FileFormatConstants;
|
||||
|
||||
namespace KeyKeeper.ViewModels;
|
||||
|
||||
@@ -44,7 +45,7 @@ public partial class RepositoryWindowViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>).
|
||||
/// Сбрасывает таймер блокировки (вызывается при любой активности пользователя).
|
||||
/// </summary>
|
||||
public void ResetLockTimer()
|
||||
{
|
||||
@@ -54,7 +55,9 @@ public partial class RepositoryWindowViewModel : ViewModelBase
|
||||
|
||||
private void SwitchToUnlocked()
|
||||
{
|
||||
CurrentPage = new UnlockedRepositoryViewModel(passStore);
|
||||
var directory = passStore.GetGroupByType(GROUP_TYPE_DEFAULT)
|
||||
?? passStore.GetRootDirectory();
|
||||
CurrentPage = new UnlockedRepositoryViewModel(passStore, directory);
|
||||
StartLockTimer();
|
||||
}
|
||||
|
||||
@@ -109,4 +112,4 @@ public partial class RepositoryWindowViewModel : ViewModelBase
|
||||
var r = remaining ?? LockTimeout;
|
||||
LockTimerDisplay = $"{r:mm\\:ss}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,39 +8,64 @@ namespace KeyKeeper.ViewModels;
|
||||
public class UnlockedRepositoryViewModel : ViewModelBase
|
||||
{
|
||||
private IPassStore passStore;
|
||||
private IPassStoreDirectory currentDirectory;
|
||||
private bool hasUnsavedChanges;
|
||||
|
||||
public IEnumerable<PassStoreEntryPassword> Passwords
|
||||
{
|
||||
get
|
||||
{
|
||||
return passStore.GetRootDirectory()
|
||||
return currentDirectory
|
||||
.Where(entry => entry is PassStoreEntryPassword)
|
||||
.Select(entry => (entry as PassStoreEntryPassword)!);
|
||||
}
|
||||
}
|
||||
|
||||
public UnlockedRepositoryViewModel(IPassStore store)
|
||||
public bool HasUnsavedChanges
|
||||
{
|
||||
get => hasUnsavedChanges;
|
||||
private set
|
||||
{
|
||||
hasUnsavedChanges = value;
|
||||
OnPropertyChanged(nameof(HasUnsavedChanges));
|
||||
}
|
||||
}
|
||||
|
||||
public UnlockedRepositoryViewModel(IPassStore store, IPassStoreDirectory directory)
|
||||
{
|
||||
passStore = store;
|
||||
currentDirectory = directory;
|
||||
HasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
public void AddEntry(PassStoreEntry entry)
|
||||
{
|
||||
if (entry is PassStoreEntryPassword)
|
||||
{
|
||||
(passStore.GetRootDirectory() as PassStoreEntryGroup)!.ChildEntries.Add(entry);
|
||||
currentDirectory.AddEntry(entry);
|
||||
HasUnsavedChanges = true;
|
||||
OnPropertyChanged(nameof(Passwords));
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteEntry(Guid id)
|
||||
{
|
||||
(passStore.GetRootDirectory() as PassStoreEntryGroup)!.DeleteEntry(id);
|
||||
currentDirectory.DeleteEntry(id);
|
||||
HasUnsavedChanges = true;
|
||||
OnPropertyChanged(nameof(Passwords));
|
||||
}
|
||||
|
||||
public void UpdateEntry(PassStoreEntryPassword updatedEntry)
|
||||
{
|
||||
currentDirectory.DeleteEntry(updatedEntry.Id);
|
||||
currentDirectory.AddEntry(updatedEntry);
|
||||
HasUnsavedChanges = true;
|
||||
OnPropertyChanged(nameof(Passwords));
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
passStore.Save();
|
||||
HasUnsavedChanges = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
src/KeyKeeper/Views/CloseConfirmationDialog.axaml
Normal file
29
src/KeyKeeper/Views/CloseConfirmationDialog.axaml
Normal file
@@ -0,0 +1,29 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="KeyKeeper.Views.CloseConfirmationDialog"
|
||||
Width="420"
|
||||
Height="170"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Title="Confirm close"
|
||||
Background="White">
|
||||
<Grid Margin="16" RowDefinitions="*,Auto">
|
||||
<TextBlock Text="Save changes before closing the storage?"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="Black"
|
||||
FontSize="16"/>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8"
|
||||
Margin="0,16,0,0">
|
||||
<Button Content="Save"
|
||||
Click="Save_Click" />
|
||||
<Button Content="Do not save"
|
||||
Click="Discard_Click" />
|
||||
<Button Content="Cancel"
|
||||
Click="Cancel_Click" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
52
src/KeyKeeper/Views/CloseConfirmationDialog.axaml.cs
Normal file
52
src/KeyKeeper/Views/CloseConfirmationDialog.axaml.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace KeyKeeper.Views;
|
||||
|
||||
public enum CloseConfirmationResult
|
||||
{
|
||||
Save,
|
||||
Discard,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
public partial class CloseConfirmationDialog : Window
|
||||
{
|
||||
private bool closingWithResult;
|
||||
|
||||
public CloseConfirmationDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnClosing(WindowClosingEventArgs e)
|
||||
{
|
||||
if (!closingWithResult)
|
||||
{
|
||||
e.Cancel = true;
|
||||
closingWithResult = true;
|
||||
Close(CloseConfirmationResult.Cancel);
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnClosing(e);
|
||||
}
|
||||
|
||||
private void Save_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
closingWithResult = true;
|
||||
Close(CloseConfirmationResult.Save);
|
||||
}
|
||||
|
||||
private void Discard_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
closingWithResult = true;
|
||||
Close(CloseConfirmationResult.Discard);
|
||||
}
|
||||
|
||||
private void Cancel_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
closingWithResult = true;
|
||||
Close(CloseConfirmationResult.Cancel);
|
||||
}
|
||||
}
|
||||
93
src/KeyKeeper/Views/CreateVaultDialog.axaml
Normal file
93
src/KeyKeeper/Views/CreateVaultDialog.axaml
Normal file
@@ -0,0 +1,93 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="KeyKeeper.Views.CreateVaultFileWindow"
|
||||
Title="Create New Vault"
|
||||
Background="#fff"
|
||||
Icon="/Assets/icon.ico"
|
||||
Width="600" Height="450"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
x:Name="ThisWindow">
|
||||
|
||||
<Grid ColumnDefinitions="1.5*, 2*">
|
||||
<!-- Левая синяя панель -->
|
||||
<Border Background="#2328C4" Grid.Column="0">
|
||||
<StackPanel VerticalAlignment="Center" Margin="20">
|
||||
<TextBlock Text="Choose where to save your password database and set a master password"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="#E0E0FF"
|
||||
FontSize="20"
|
||||
TextAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Правая белая панель с формой -->
|
||||
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="30" VerticalAlignment="Center" Spacing="20">
|
||||
<TextBlock Text="Create new vault"
|
||||
FontSize="22"
|
||||
FontWeight="Bold"
|
||||
Foreground="#2328C4"/>
|
||||
|
||||
<!-- Выбор файла -->
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="File location" FontWeight="SemiBold" Foreground="Black" />
|
||||
<Grid ColumnDefinitions="*, Auto" ColumnSpacing="10">
|
||||
<TextBox x:Name="FilePathTextBox"
|
||||
Grid.Column="0"
|
||||
Watermark="Select file path..."
|
||||
Padding="10,8"/>
|
||||
<Button x:Name="BrowseButton"
|
||||
Grid.Column="1"
|
||||
Content="Browse..."
|
||||
Classes="secondaryButton"
|
||||
Padding="15,8"
|
||||
Click="BrowseButton_Click"/>
|
||||
</Grid>
|
||||
<TextBlock x:Name="PathWarning"
|
||||
FontSize="12"
|
||||
Foreground="Orange"
|
||||
Text=" "/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Ввод мастер-пароля -->
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Master password" FontWeight="SemiBold" Foreground="Black" />
|
||||
<TextBox x:Name="PasswordBox"
|
||||
PasswordChar="*"
|
||||
Watermark="Enter password"
|
||||
Padding="10,8"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Confirm password" FontWeight="SemiBold" Foreground="Black" />
|
||||
<TextBox x:Name="ConfirmPasswordBox"
|
||||
PasswordChar="*"
|
||||
Watermark="Confirm password"
|
||||
Padding="10,8"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Сообщение об ошибке пароля -->
|
||||
<TextBlock x:Name="PasswordErrorText"
|
||||
FontSize="12"
|
||||
Foreground="Red"
|
||||
Text=""
|
||||
IsVisible="False"/>
|
||||
|
||||
<!-- Кнопки действий -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10" Margin="0,20,0,0">
|
||||
<Button Content="Cancel"
|
||||
Classes="secondaryButton"
|
||||
Width="80"
|
||||
Click="CancelButton_Click"/>
|
||||
<Button x:Name="CreateButton"
|
||||
Content="Create"
|
||||
Classes="accentButton"
|
||||
Width="80"
|
||||
IsEnabled="False"
|
||||
Click="CreateButton_Click"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Window>
|
||||
124
src/KeyKeeper/Views/CreateVaultDialog.axaml.cs
Normal file
124
src/KeyKeeper/Views/CreateVaultDialog.axaml.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KeyKeeper.Views
|
||||
{
|
||||
public partial class CreateVaultFileWindow : Window
|
||||
{
|
||||
public string FilePath { get; private set; } = string.Empty;
|
||||
public string Password { get; private set; } = string.Empty;
|
||||
public bool Success { get; private set; }
|
||||
|
||||
public CreateVaultFileWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
FilePathTextBox.TextChanged += OnTextChanged;
|
||||
PasswordBox.TextChanged += OnPasswordTextChanged;
|
||||
ConfirmPasswordBox.TextChanged += OnPasswordTextChanged;
|
||||
}
|
||||
|
||||
private async void OnTextChanged(object? sender, TextChangedEventArgs e)
|
||||
{
|
||||
UpdateCreateButtonState();
|
||||
PathWarning.Text = "";
|
||||
|
||||
string path = FilePathTextBox.Text ?? "";
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var storageFile = await StorageProvider.TryGetFileFromPathAsync(path);
|
||||
if (storageFile != null)
|
||||
{
|
||||
PathWarning.Text = "File already exists. It will be overwritten.";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPasswordTextChanged(object? sender, TextChangedEventArgs e)
|
||||
{
|
||||
UpdateCreateButtonState();
|
||||
PasswordErrorText.IsVisible = false;
|
||||
}
|
||||
|
||||
private void UpdateCreateButtonState()
|
||||
{
|
||||
bool pathValid = !string.IsNullOrWhiteSpace(FilePathTextBox.Text);
|
||||
bool passwordsEntered = !string.IsNullOrWhiteSpace(PasswordBox.Text) &&
|
||||
!string.IsNullOrWhiteSpace(ConfirmPasswordBox.Text);
|
||||
CreateButton.IsEnabled = pathValid && passwordsEntered;
|
||||
}
|
||||
|
||||
private async void BrowseButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Create new password store",
|
||||
SuggestedFileName = "passwords.kkp",
|
||||
DefaultExtension = "kkp",
|
||||
FileTypeChoices = new[]
|
||||
{
|
||||
new FilePickerFileType("KeyKeeper files")
|
||||
{
|
||||
Patterns = new[] { "*.kkp" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (file?.TryGetLocalPath() is string path)
|
||||
{
|
||||
FilePathTextBox.Text = path;
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
string path = FilePathTextBox.Text ?? "";
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return;
|
||||
|
||||
string password = PasswordBox.Text ?? "";
|
||||
string confirm = ConfirmPasswordBox.Text ?? "";
|
||||
|
||||
if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(confirm))
|
||||
{
|
||||
ShowPasswordError("Password cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password != confirm)
|
||||
{
|
||||
ShowPasswordError("Passwords don't match");
|
||||
return;
|
||||
}
|
||||
|
||||
FilePath = path;
|
||||
Password = password;
|
||||
Success = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void ShowPasswordError(string message)
|
||||
{
|
||||
PasswordErrorText.Text = message;
|
||||
PasswordErrorText.IsVisible = true;
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Success = false;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace KeyKeeper.Views;
|
||||
public partial class EntryEditWindow : Window
|
||||
{
|
||||
public PassStoreEntryPassword? EditedEntry;
|
||||
private PassStoreEntryPassword? _originalEntry;
|
||||
|
||||
public EntryEditWindow()
|
||||
{
|
||||
@@ -21,6 +22,14 @@ public partial class EntryEditWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
public void SetEntry(PassStoreEntryPassword entry)
|
||||
{
|
||||
_originalEntry = entry;
|
||||
EntryNameEdit.Text = entry.Name;
|
||||
UsernameEdit.Text = entry.Username.Value;
|
||||
PasswordEdit.Text = entry.Password.Value;
|
||||
}
|
||||
|
||||
private void PasswordTextChanged(object? sender, TextChangedEventArgs e)
|
||||
{
|
||||
string password = PasswordEdit?.Text ?? "";
|
||||
@@ -39,49 +48,34 @@ public partial class EntryEditWindow : Window
|
||||
}
|
||||
|
||||
int strength = CalculatePasswordStrength(password);
|
||||
|
||||
double maxWidth = PasswordStrengthIndicator.Bounds.Width;
|
||||
if (maxWidth <= 0) maxWidth = 200;
|
||||
if (maxWidth <= 0) maxWidth = 200;
|
||||
|
||||
PasswordStrengthFill.Width = (strength / 100.0) * maxWidth;
|
||||
|
||||
if (strength < 20)
|
||||
{
|
||||
PasswordStrengthFill.Background = new SolidColorBrush(Colors.Red);
|
||||
}
|
||||
else if (strength < 50)
|
||||
{
|
||||
PasswordStrengthFill.Background = new SolidColorBrush(Colors.Orange);
|
||||
}
|
||||
else if (strength < 70)
|
||||
{
|
||||
PasswordStrengthFill.Background = new SolidColorBrush(Colors.Gold);
|
||||
}
|
||||
else
|
||||
{
|
||||
PasswordStrengthFill.Background = new SolidColorBrush(Colors.Green);
|
||||
}
|
||||
}
|
||||
|
||||
private int CalculatePasswordStrength(string password)
|
||||
{
|
||||
int score = 0;
|
||||
|
||||
if (password.Length >= 8) score += 20;
|
||||
if (password.Length >= 12) score += 20;
|
||||
if (password.Length >= 16) score += 15;
|
||||
|
||||
if (Regex.IsMatch(password, @"\d")) score += 10;
|
||||
|
||||
if (Regex.IsMatch(password, @"[a-z]")) score += 15;
|
||||
|
||||
if (Regex.IsMatch(password, @"[A-Z]")) score += 15;
|
||||
|
||||
if (Regex.IsMatch(password, @"[!@#$%^&*()_+\-=\[\]{};':""\\|,.<>\/?]")) score += 20;
|
||||
|
||||
var uniqueChars = new System.Collections.Generic.HashSet<char>(password).Count;
|
||||
score += Math.Min(20, uniqueChars * 2);
|
||||
|
||||
return Math.Min(100, score);
|
||||
}
|
||||
|
||||
@@ -96,22 +90,17 @@ public partial class EntryEditWindow : Window
|
||||
string password = PasswordEdit?.Text ?? "";
|
||||
if (string.IsNullOrEmpty(password)) return;
|
||||
|
||||
Guid id = _originalEntry?.Id ?? Guid.NewGuid();
|
||||
DateTime created = DateTime.UtcNow;
|
||||
|
||||
EditedEntry = new PassStoreEntryPassword(
|
||||
Guid.NewGuid(),
|
||||
DateTime.UtcNow,
|
||||
id,
|
||||
created,
|
||||
DateTime.UtcNow,
|
||||
EntryIconType.DEFAULT,
|
||||
name,
|
||||
new LoginField()
|
||||
{
|
||||
Type = LOGIN_FIELD_USERNAME_ID,
|
||||
Value = username
|
||||
},
|
||||
new LoginField()
|
||||
{
|
||||
Type = LOGIN_FIELD_PASSWORD_ID,
|
||||
Value = password
|
||||
},
|
||||
new LoginField() { Type = LOGIN_FIELD_USERNAME_ID, Value = username },
|
||||
new LoginField() { Type = LOGIN_FIELD_PASSWORD_ID, Value = password },
|
||||
null
|
||||
);
|
||||
Close();
|
||||
|
||||
@@ -22,41 +22,26 @@ namespace KeyKeeper.Views
|
||||
|
||||
private async void CreateNewVault_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Create new password store",
|
||||
SuggestedFileName = "passwords.kkp",
|
||||
DefaultExtension = "kkp",
|
||||
FileTypeChoices = new[]
|
||||
{
|
||||
new FilePickerFileType("KeyKeeper files")
|
||||
{
|
||||
Patterns = new[] { "*.kkp" }
|
||||
}
|
||||
}
|
||||
});
|
||||
var createVaultDialog = new CreateVaultFileWindow();
|
||||
await createVaultDialog.ShowDialog(this);
|
||||
|
||||
if (file != null)
|
||||
if (createVaultDialog.Success &&
|
||||
!string.IsNullOrEmpty(createVaultDialog.FilePath) &&
|
||||
!string.IsNullOrEmpty(createVaultDialog.Password))
|
||||
{
|
||||
if (file.TryGetLocalPath() is string path)
|
||||
{
|
||||
var passwordDialog = new PasswordDialog();
|
||||
await passwordDialog.ShowDialog(this);
|
||||
if (passwordDialog.Created && !string.IsNullOrEmpty(passwordDialog.Password))
|
||||
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()
|
||||
{
|
||||
var compositeKey = new CompositeKey(passwordDialog.Password, null);
|
||||
var passStoreAccessor = new PassStoreFileAccessor(
|
||||
filename: path,
|
||||
create: true,
|
||||
createOptions: new StoreCreationOptions()
|
||||
{
|
||||
Key = compositeKey,
|
||||
LockTimeoutSeconds = 800
|
||||
});
|
||||
IPassStore passStore = passStoreAccessor;
|
||||
OpenRepositoryWindow(passStore);
|
||||
}
|
||||
}
|
||||
Key = compositeKey,
|
||||
LockTimeoutSeconds = 800
|
||||
});
|
||||
IPassStore passStore = passStoreAccessor;
|
||||
OpenRepositoryWindow(passStore);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="KeyKeeper.Views.PasswordDialog"
|
||||
Title="Создание хранилища"
|
||||
Width="350"
|
||||
Height="230">
|
||||
|
||||
<StackPanel Margin="20" VerticalAlignment="Center">
|
||||
|
||||
<TextBlock Text="Set Master Password"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Margin="0,0,0,15"/>
|
||||
|
||||
<!-- Поле для пароля (звездочки) -->
|
||||
<StackPanel Margin="0,0,0,10">
|
||||
<TextBlock Text="Password:" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="PasswordBox"
|
||||
PasswordChar="*"
|
||||
Width="250"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Поле для подтверждения (звездочки) -->
|
||||
<StackPanel Margin="0,0,0,15">
|
||||
<TextBlock Text="Confirm Password:" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="ConfirmPasswordBox"
|
||||
PasswordChar="*"
|
||||
Width="250"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Сообщение об ошибке -->
|
||||
<TextBlock x:Name="ErrorText"
|
||||
Text="Passwords do not match"
|
||||
Foreground="Red"
|
||||
FontSize="12"
|
||||
Margin="0,0,0,10"
|
||||
IsVisible="False"/>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="10">
|
||||
<Button Content="Cancel"
|
||||
Width="80"
|
||||
Height="30"
|
||||
Click="CancelButton_Click"/>
|
||||
|
||||
<Button Content="Create"
|
||||
Width="80"
|
||||
Height="30"
|
||||
Click="CreateButton_Click"/>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Window>
|
||||
@@ -1,65 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace KeyKeeper.Views
|
||||
{
|
||||
public partial class PasswordDialog : Window
|
||||
{
|
||||
public string Password { get; private set; } = "";
|
||||
public bool Created { get; private set; } = false;
|
||||
|
||||
public PasswordDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void CreateButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var passwordBox = this.FindControl<TextBox>("PasswordBox");
|
||||
var confirmBox = this.FindControl<TextBox>("ConfirmPasswordBox");
|
||||
var errorText = this.FindControl<TextBlock>("ErrorText");
|
||||
|
||||
string password = passwordBox?.Text ?? "";
|
||||
string confirmPassword = confirmBox?.Text ?? "";
|
||||
|
||||
if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(confirmPassword))
|
||||
{
|
||||
ShowError("Password cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password != confirmPassword)
|
||||
{
|
||||
ShowError("Passwords don't match");
|
||||
return;
|
||||
}
|
||||
|
||||
Password = password;
|
||||
Created = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
var errorText = this.FindControl<TextBlock>("ErrorText");
|
||||
if (errorText != null)
|
||||
{
|
||||
errorText.Text = message;
|
||||
errorText.IsVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Created = false;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:i="using:Avalonia.Interactivity"
|
||||
xmlns:kkp="using:KeyKeeper.Views"
|
||||
x:Class="KeyKeeper.Views.RepositoryWindow"
|
||||
Closing="RepositoryWindow_Closing"
|
||||
Title="KeyKeeper - Password store"
|
||||
CanResize="False"
|
||||
Width="800"
|
||||
@@ -11,21 +12,21 @@
|
||||
Background="White"
|
||||
x:DataType="vm:RepositoryWindowViewModel">
|
||||
|
||||
<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"
|
||||
FontSize="32"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,0,0,8"/>
|
||||
<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"
|
||||
FontSize="32"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,0,0,20"/>
|
||||
|
||||
<!-- Таймер блокировки под надписью KeyKeeper -->
|
||||
<Border Background="#CC000000"
|
||||
@@ -51,21 +52,21 @@
|
||||
</Border>
|
||||
|
||||
<!-- Рамочка -->
|
||||
<Border BorderBrush="Gray"
|
||||
<!-- <Border BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Padding="20"
|
||||
Background="#F5F5F5"
|
||||
HorizontalAlignment="Left">
|
||||
|
||||
<StackPanel HorizontalAlignment="Left">
|
||||
<Button Content="All Passwords"
|
||||
Width="120"
|
||||
Height="30"
|
||||
HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Left">
|
||||
<Button Content="All Passwords"
|
||||
Width="120"
|
||||
Height="30"
|
||||
HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
</Border> -->
|
||||
<!-- Save Passwords -->
|
||||
<Button Content="Save Passwords"
|
||||
Classes="accentSidebarButton"
|
||||
@@ -74,73 +75,93 @@
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,20,0,0"/>
|
||||
|
||||
<Button Content="New Entry"
|
||||
Classes="accentSidebarButton"
|
||||
Click="AddEntryButton_Click"
|
||||
Height="30"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,20,0,0"/>
|
||||
</StackPanel>
|
||||
<ListBox Width="580"
|
||||
Margin="210 10 10 10"
|
||||
ItemsSource="{Binding Passwords}"
|
||||
Background="Transparent">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<!-- New Entry -->
|
||||
<Button Content="New Entry"
|
||||
Classes="accentSidebarButton"
|
||||
Click="AddEntryButton_Click"
|
||||
Height="30"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,20,0,0"/>
|
||||
|
||||
<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}"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="Black" />
|
||||
<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"/>
|
||||
<MenuItem Name="entryCtxMenuDelete" Header="Delete" Click="EntryContextMenuItem_Click"/>
|
||||
</ContextMenu>
|
||||
</Border.ContextMenu>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<kkp:ToastNotificationHost x:Name="NotificationHost" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20" Duration="0:0:2" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:LockedRepositoryViewModel}">
|
||||
<StackPanel Margin="20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="10">
|
||||
<TextBlock Text="Enter credentials to unlock"
|
||||
Foreground="#2328C4"
|
||||
FontSize="32" />
|
||||
<!-- Edit Selected Entry -->
|
||||
<Button Content="Edit Selected Entry"
|
||||
Classes="accentSidebarButton"
|
||||
Click="EditEntryButton_Click"
|
||||
Height="30"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,20,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBox x:Name="UnlockPasswordEdit"
|
||||
Text="{Binding UnlockPassword, Mode=TwoWay}"
|
||||
PasswordChar="*"
|
||||
Width="450" />
|
||||
<!-- 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>
|
||||
|
||||
<Button x:Name="UnlockButton"
|
||||
Command="{Binding TryUnlock}"
|
||||
HorizontalAlignment="Center"
|
||||
Content="Unlock!" />
|
||||
<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}"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="Black" />
|
||||
<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>
|
||||
</Window.DataTemplates>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<ContentControl Content="{Binding CurrentPage}"/>
|
||||
<kkp:ToastNotificationHost x:Name="NotificationHost"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="20"
|
||||
Duration="0:0:2" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type vm:LockedRepositoryViewModel}">
|
||||
<StackPanel Margin="20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="10">
|
||||
<TextBlock Text="Enter credentials to unlock"
|
||||
Foreground="#2328C4"
|
||||
FontSize="32" />
|
||||
|
||||
<TextBox x:Name="UnlockPasswordEdit"
|
||||
Text="{Binding UnlockPassword, Mode=TwoWay}"
|
||||
PasswordChar="*"
|
||||
Width="450" />
|
||||
|
||||
<Button x:Name="UnlockButton"
|
||||
Command="{Binding TryUnlock}"
|
||||
HorizontalAlignment="Center"
|
||||
Content="Unlock!" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Window.DataTemplates>
|
||||
|
||||
<ContentControl Content="{Binding CurrentPage}"/>
|
||||
</Window>
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace KeyKeeper.Views;
|
||||
|
||||
public partial class RepositoryWindow : Window
|
||||
{
|
||||
private bool allowClose;
|
||||
private bool closeConfirmationShown;
|
||||
|
||||
public RepositoryWindow(RepositoryWindowViewModel model)
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -36,6 +39,48 @@ public partial class RepositoryWindow : Window
|
||||
vm.ResetLockTimer();
|
||||
}
|
||||
|
||||
private async void RepositoryWindow_Closing(object? sender, WindowClosingEventArgs e)
|
||||
{
|
||||
if (allowClose || closeConfirmationShown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (DataContext is RepositoryWindowViewModel checkVm)
|
||||
{
|
||||
if ((checkVm.CurrentPage is UnlockedRepositoryViewModel unlockedVm &&
|
||||
!unlockedVm.HasUnsavedChanges)
|
||||
|| checkVm.CurrentPage is LockedRepositoryViewModel)
|
||||
{
|
||||
allowClose = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
e.Cancel = true;
|
||||
closeConfirmationShown = true;
|
||||
|
||||
var dialog = new CloseConfirmationDialog();
|
||||
var result = await dialog.ShowDialog<CloseConfirmationResult?>(this);
|
||||
|
||||
closeConfirmationShown = false;
|
||||
|
||||
if (result == null || result == CloseConfirmationResult.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == CloseConfirmationResult.Save &&
|
||||
DataContext is RepositoryWindowViewModel vm &&
|
||||
vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
|
||||
{
|
||||
pageVm.Save();
|
||||
}
|
||||
|
||||
allowClose = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private async void AddEntryButton_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (DataContext is RepositoryWindowViewModel vm_ && vm_.CurrentPage is UnlockedRepositoryViewModel vm)
|
||||
@@ -53,6 +98,36 @@ public partial class RepositoryWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
private async void EditEntryButton_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (DataContext is RepositoryWindowViewModel vm_ && vm_.CurrentPage is UnlockedRepositoryViewModel vm)
|
||||
{
|
||||
var listBox = this.FindControlRecursive<ListBox>("PasswordsListBox");
|
||||
if (listBox == null)
|
||||
{
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("ListBox not found");
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedEntry = listBox.SelectedItem as PassStoreEntryPassword;
|
||||
if (selectedEntry == null)
|
||||
{
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("No entry selected");
|
||||
return;
|
||||
}
|
||||
|
||||
EntryEditWindow dialog = new();
|
||||
dialog.SetEntry(selectedEntry);
|
||||
await dialog.ShowDialog(this);
|
||||
|
||||
if (dialog.EditedEntry != null)
|
||||
{
|
||||
vm.UpdateEntry(dialog.EditedEntry);
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Entry updated");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveButton_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
|
||||
@@ -63,41 +138,46 @@ public partial class RepositoryWindow : Window
|
||||
|
||||
private void Entry_DoubleTapped(object sender, TappedEventArgs args)
|
||||
{
|
||||
if (args.Source is StyledElement s)
|
||||
if (args.Source is StyledElement s && s.DataContext is PassStoreEntryPassword pwd)
|
||||
{
|
||||
if (s.DataContext is PassStoreEntryPassword pwd)
|
||||
Clipboard!.SetTextAsync(pwd.Password.Value);
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Password copied to clipboard");
|
||||
}
|
||||
}
|
||||
|
||||
private async void EntryContextMenuItem_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (args.Source is StyledElement s && s.DataContext is PassStoreEntryPassword pwd)
|
||||
{
|
||||
if (s.Name == "entryCtxMenuCopyUsername")
|
||||
{
|
||||
Clipboard!.SetTextAsync(pwd.Username.Value);
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Username copied to clipboard");
|
||||
}
|
||||
else if (s.Name == "entryCtxMenuCopyPassword")
|
||||
{
|
||||
Clipboard!.SetTextAsync(pwd.Password.Value);
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Password copied to clipboard");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EntryContextMenuItem_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (args.Source is StyledElement s)
|
||||
{
|
||||
if (s.DataContext is PassStoreEntryPassword pwd)
|
||||
else if (s.Name == "entryCtxMenuEdit")
|
||||
{
|
||||
if (s.Name == "entryCtxMenuCopyUsername")
|
||||
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
|
||||
{
|
||||
Clipboard!.SetTextAsync(pwd.Username.Value);
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Username copied to clipboard");
|
||||
}
|
||||
else if (s.Name == "entryCtxMenuCopyPassword")
|
||||
{
|
||||
Clipboard!.SetTextAsync(pwd.Password.Value);
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Password copied to clipboard");
|
||||
}
|
||||
else if (s.Name == "entryCtxMenuDelete")
|
||||
{
|
||||
if (s.DataContext is PassStoreEntryPassword entry)
|
||||
EntryEditWindow dialog = new();
|
||||
dialog.SetEntry(pwd);
|
||||
await dialog.ShowDialog(this);
|
||||
if (dialog.EditedEntry != null)
|
||||
{
|
||||
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
|
||||
{
|
||||
pageVm.DeleteEntry(entry.Id);
|
||||
}
|
||||
pageVm.UpdateEntry(dialog.EditedEntry);
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Entry updated");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (s.Name == "entryCtxMenuDelete")
|
||||
{
|
||||
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
|
||||
{
|
||||
pageVm.DeleteEntry(pwd.Id);
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Entry deleted");
|
||||
}
|
||||
}
|
||||
@@ -114,34 +194,26 @@ public static class VisualTreeExtensions
|
||||
|
||||
private static T? FindControlRecursiveInternal<T>(Visual parent, string name, int depth) where T : Visual
|
||||
{
|
||||
if (parent == null)
|
||||
return null;
|
||||
|
||||
if (parent is T t && parent.Name == name)
|
||||
return t;
|
||||
if (parent == null) return null;
|
||||
if (parent is T t && parent.Name == name) return t;
|
||||
|
||||
foreach (var child in parent.GetVisualChildren())
|
||||
{
|
||||
if (child == null)
|
||||
continue;
|
||||
|
||||
if (child == null) continue;
|
||||
var result = FindControlRecursiveInternal<T>(child, name, depth + 1);
|
||||
if (result != null)
|
||||
return result;
|
||||
if (result != null) return result;
|
||||
}
|
||||
|
||||
// Also check logical children if they're not in visual tree
|
||||
if (parent is ContentPresenter contentPresenter)
|
||||
{
|
||||
var content = contentPresenter.Content as Visual;
|
||||
if (content != null)
|
||||
{
|
||||
var result = FindControlRecursiveInternal<T>(content, name, depth + 1);
|
||||
if (result != null)
|
||||
return result;
|
||||
if (result != null) return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user