Feat/EditEntry

This commit is contained in:
Быстров Михаил Евгеньевич
2026-03-01 14:58:00 +03:00
parent 2aeb67d14b
commit f237f0fe8f
4 changed files with 217 additions and 170 deletions

View File

@@ -39,6 +39,16 @@ public class UnlockedRepositoryViewModel : ViewModelBase
OnPropertyChanged(nameof(Passwords)); OnPropertyChanged(nameof(Passwords));
} }
public void UpdateEntry(PassStoreEntryPassword updatedEntry)
{
var root = passStore.GetRootDirectory() as PassStoreEntryGroup;
if (root == null) return;
root.DeleteEntry(updatedEntry.Id);
root.ChildEntries.Add(updatedEntry);
OnPropertyChanged(nameof(Passwords));
}
public void Save() public void Save()
{ {
passStore.Save(); passStore.Save();

View File

@@ -11,6 +11,7 @@ namespace KeyKeeper.Views;
public partial class EntryEditWindow : Window public partial class EntryEditWindow : Window
{ {
public PassStoreEntryPassword? EditedEntry; public PassStoreEntryPassword? EditedEntry;
private PassStoreEntryPassword? _originalEntry;
public EntryEditWindow() 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) private void PasswordTextChanged(object? sender, TextChangedEventArgs e)
{ {
string password = PasswordEdit?.Text ?? ""; string password = PasswordEdit?.Text ?? "";
@@ -39,49 +48,34 @@ public partial class EntryEditWindow : Window
} }
int strength = CalculatePasswordStrength(password); int strength = CalculatePasswordStrength(password);
double maxWidth = PasswordStrengthIndicator.Bounds.Width; double maxWidth = PasswordStrengthIndicator.Bounds.Width;
if (maxWidth <= 0) maxWidth = 200; if (maxWidth <= 0) maxWidth = 200;
PasswordStrengthFill.Width = (strength / 100.0) * maxWidth; PasswordStrengthFill.Width = (strength / 100.0) * maxWidth;
if (strength < 20) if (strength < 20)
{
PasswordStrengthFill.Background = new SolidColorBrush(Colors.Red); PasswordStrengthFill.Background = new SolidColorBrush(Colors.Red);
}
else if (strength < 50) else if (strength < 50)
{
PasswordStrengthFill.Background = new SolidColorBrush(Colors.Orange); PasswordStrengthFill.Background = new SolidColorBrush(Colors.Orange);
}
else if (strength < 70) else if (strength < 70)
{
PasswordStrengthFill.Background = new SolidColorBrush(Colors.Gold); PasswordStrengthFill.Background = new SolidColorBrush(Colors.Gold);
}
else else
{
PasswordStrengthFill.Background = new SolidColorBrush(Colors.Green); PasswordStrengthFill.Background = new SolidColorBrush(Colors.Green);
}
} }
private int CalculatePasswordStrength(string password) private int CalculatePasswordStrength(string password)
{ {
int score = 0; int score = 0;
if (password.Length >= 8) score += 20; if (password.Length >= 8) score += 20;
if (password.Length >= 12) score += 20; if (password.Length >= 12) score += 20;
if (password.Length >= 16) score += 15; if (password.Length >= 16) score += 15;
if (Regex.IsMatch(password, @"\d")) score += 10; 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, @"[A-Z]")) score += 15; if (Regex.IsMatch(password, @"[A-Z]")) score += 15;
if (Regex.IsMatch(password, @"[!@#$%^&*()_+\-=\[\]{};':""\\|,.<>\/?]")) score += 20; if (Regex.IsMatch(password, @"[!@#$%^&*()_+\-=\[\]{};':""\\|,.<>\/?]")) score += 20;
var uniqueChars = new System.Collections.Generic.HashSet<char>(password).Count; var uniqueChars = new System.Collections.Generic.HashSet<char>(password).Count;
score += Math.Min(20, uniqueChars * 2); score += Math.Min(20, uniqueChars * 2);
return Math.Min(100, score); return Math.Min(100, score);
} }
@@ -96,22 +90,17 @@ public partial class EntryEditWindow : Window
string password = PasswordEdit?.Text ?? ""; string password = PasswordEdit?.Text ?? "";
if (string.IsNullOrEmpty(password)) return; if (string.IsNullOrEmpty(password)) return;
Guid id = _originalEntry?.Id ?? Guid.NewGuid();
DateTime created = DateTime.UtcNow;
EditedEntry = new PassStoreEntryPassword( EditedEntry = new PassStoreEntryPassword(
Guid.NewGuid(), id,
DateTime.UtcNow, created,
DateTime.UtcNow, DateTime.UtcNow,
EntryIconType.DEFAULT, EntryIconType.DEFAULT,
name, name,
new LoginField() new LoginField() { Type = LOGIN_FIELD_USERNAME_ID, Value = username },
{ new LoginField() { Type = LOGIN_FIELD_PASSWORD_ID, Value = password },
Type = LOGIN_FIELD_USERNAME_ID,
Value = username
},
new LoginField()
{
Type = LOGIN_FIELD_PASSWORD_ID,
Value = password
},
null null
); );
Close(); Close();

View File

@@ -11,113 +11,133 @@
Background="White" Background="White"
x:DataType="vm:RepositoryWindowViewModel"> x:DataType="vm:RepositoryWindowViewModel">
<Window.DataTemplates> <Window.DataTemplates>
<DataTemplate DataType="{x:Type vm:UnlockedRepositoryViewModel}"> <DataTemplate DataType="{x:Type vm:UnlockedRepositoryViewModel}">
<Grid> <Grid>
<!-- Синий левый край --> <!-- Синий левый край -->
<Border Width="200" <Border Width="200"
Background="#2328C4" 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"/>
<!-- Рамочка -->
<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>
</Border>
<!-- Save Passwords -->
<Button Content="Save Passwords"
Classes="accentSidebarButton"
Click="SaveButton_Click"
Height="30"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Stretch"/> Margin="0,20,0,0"/>
<StackPanel Margin="20" HorizontalAlignment="Left">
<!-- Надпись KeyKeeper -->
<TextBlock Text="KeyKeeper"
FontSize="32"
FontWeight="Bold"
HorizontalAlignment="Left"
Margin="0,0,0,20"/>
<!-- Рамочка --> <!-- New Entry -->
<Border BorderBrush="Gray" <Button Content="New Entry"
BorderThickness="1" Classes="accentSidebarButton"
CornerRadius="5" Click="AddEntryButton_Click"
Padding="20" Height="30"
Background="#F5F5F5" HorizontalAlignment="Left"
HorizontalAlignment="Left"> Margin="0,20,0,0"/>
<StackPanel HorizontalAlignment="Left"> <!-- Edit Selected Entry -->
<Button Content="All Passwords" <Button Content="Edit Selected Entry"
Width="120" Classes="accentSidebarButton"
Height="30" Click="EditEntryButton_Click"
HorizontalAlignment="Left"/> Height="30"
</StackPanel> HorizontalAlignment="Left"
Margin="0,20,0,0"/>
</StackPanel>
</Border> <!-- ListBox с паролями -->
<!-- Save Passwords --> <ListBox x:Name="PasswordsListBox"
<Button Content="Save Passwords" Width="580"
Classes="accentSidebarButton" Margin="210 10 10 10"
Click="SaveButton_Click" ItemsSource="{Binding Passwords}"
Height="30" Background="Transparent"
HorizontalAlignment="Left" SelectionMode="Single">
Margin="0,20,0,0"/> <ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<Button Content="New Entry" <ListBox.ItemTemplate>
Classes="accentSidebarButton" <DataTemplate>
Click="AddEntryButton_Click" <Border Background="Transparent" DoubleTapped="Entry_DoubleTapped">
Height="30" <StackPanel Width="100"
HorizontalAlignment="Left" Margin="10"
Margin="0,20,0,0"/> HorizontalAlignment="Center">
</StackPanel> <Svg Path="{Binding IconPath}" Width="48" Height="48"/>
<ListBox Width="580" <TextBlock Text="{Binding Name}"
Margin="210 10 10 10" HorizontalAlignment="Center"
ItemsSource="{Binding Passwords}" Foreground="Black" />
Background="Transparent"> <TextBlock Text="{Binding Username.Value}"
<ListBox.ItemsPanel> Foreground="#666"
<ItemsPanelTemplate> HorizontalAlignment="Center" />
<WrapPanel Orientation="Horizontal" /> </StackPanel>
</ItemsPanelTemplate> <Border.ContextMenu>
</ListBox.ItemsPanel> <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>
<ListBox.ItemTemplate> <kkp:ToastNotificationHost x:Name="NotificationHost"
<DataTemplate> HorizontalAlignment="Center"
<Border Background="Transparent" DoubleTapped="Entry_DoubleTapped"> VerticalAlignment="Center"
<StackPanel Width="100" Margin="20"
Margin="10" Duration="0:0:2" />
HorizontalAlignment="Center"> </Grid>
<Svg Path="{Binding IconPath}" Width="48" Height="48" </DataTemplate>
/>
<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" />
<TextBox x:Name="UnlockPasswordEdit" <DataTemplate DataType="{x:Type vm:LockedRepositoryViewModel}">
Text="{Binding UnlockPassword, Mode=TwoWay}" <StackPanel Margin="20"
PasswordChar="*" HorizontalAlignment="Center"
Width="450" /> VerticalAlignment="Center"
Spacing="10">
<TextBlock Text="Enter credentials to unlock"
Foreground="#2328C4"
FontSize="32" />
<Button x:Name="UnlockButton" <TextBox x:Name="UnlockPasswordEdit"
Command="{Binding TryUnlock}" Text="{Binding UnlockPassword, Mode=TwoWay}"
HorizontalAlignment="Center" PasswordChar="*"
Content="Unlock!" /> Width="450" />
</StackPanel>
</DataTemplate>
</Window.DataTemplates>
<ContentControl Content="{Binding CurrentPage}"/> <Button x:Name="UnlockButton"
Command="{Binding TryUnlock}"
HorizontalAlignment="Center"
Content="Unlock!" />
</StackPanel>
</DataTemplate>
</Window.DataTemplates>
<ContentControl Content="{Binding CurrentPage}"/>
</Window> </Window>

View File

@@ -10,7 +10,7 @@ using Avalonia.Controls.Presenters;
namespace KeyKeeper.Views; namespace KeyKeeper.Views;
public partial class RepositoryWindow: Window public partial class RepositoryWindow : Window
{ {
public RepositoryWindow(RepositoryWindowViewModel model) public RepositoryWindow(RepositoryWindowViewModel model)
{ {
@@ -39,6 +39,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) private void SaveButton_Click(object sender, RoutedEventArgs args)
{ {
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm) if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
@@ -49,40 +79,46 @@ public partial class RepositoryWindow: Window
private void Entry_DoubleTapped(object sender, TappedEventArgs args) 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); Clipboard!.SetTextAsync(pwd.Password.Value);
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Password copied to clipboard"); this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Password copied to clipboard");
} }
} else if (s.Name == "entryCtxMenuEdit")
}
private void EntryContextMenuItem_Click(object sender, RoutedEventArgs args) {
if (args.Source is StyledElement s)
{
if (s.DataContext is PassStoreEntryPassword pwd)
{ {
if (s.Name == "entryCtxMenuCopyUsername") if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
{ {
Clipboard!.SetTextAsync(pwd.Username.Value); EntryEditWindow dialog = new();
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Username copied to clipboard"); dialog.SetEntry(pwd);
} await dialog.ShowDialog(this);
else if (s.Name == "entryCtxMenuCopyPassword") if (dialog.EditedEntry != null)
{
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)
{ {
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm) pageVm.UpdateEntry(dialog.EditedEntry);
{ this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Entry updated");
pageVm.DeleteEntry(entry.Id);
}
} }
}
}
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"); this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Entry deleted");
} }
} }
@@ -99,31 +135,23 @@ public static class VisualTreeExtensions
private static T? FindControlRecursiveInternal<T>(Visual parent, string name, int depth) where T : Visual private static T? FindControlRecursiveInternal<T>(Visual parent, string name, int depth) where T : Visual
{ {
if (parent == null) if (parent == null) return null;
return null; if (parent is T t && parent.Name == name) return t;
if (parent is T t && parent.Name == name)
return t;
foreach (var child in parent.GetVisualChildren()) foreach (var child in parent.GetVisualChildren())
{ {
if (child == null) if (child == null) continue;
continue;
var result = FindControlRecursiveInternal<T>(child, name, depth + 1); var result = FindControlRecursiveInternal<T>(child, name, depth + 1);
if (result != null) if (result != null) return result;
return result;
} }
// Also check logical children if they're not in visual tree
if (parent is ContentPresenter contentPresenter) if (parent is ContentPresenter contentPresenter)
{ {
var content = contentPresenter.Content as Visual; var content = contentPresenter.Content as Visual;
if (content != null) if (content != null)
{ {
var result = FindControlRecursiveInternal<T>(content, name, depth + 1); var result = FindControlRecursiveInternal<T>(content, name, depth + 1);
if (result != null) if (result != null) return result;
return result;
} }
} }