diff --git a/src/KeyKeeper/PasswordStore/FileFormatConstants.cs b/src/KeyKeeper/PasswordStore/FileFormatConstants.cs index fc0dc28..f9e9b92 100644 --- a/src/KeyKeeper/PasswordStore/FileFormatConstants.cs +++ b/src/KeyKeeper/PasswordStore/FileFormatConstants.cs @@ -19,6 +19,7 @@ static class FileFormatConstants public const uint FILE_FIELD_END = 0x010ba81a; public const byte ENTRY_PASS_ID = 0x00; public const byte ENTRY_GROUP_ID = 0x01; + public const byte ENTRY_LINK_ID = 0x02; public const byte LOGIN_FIELD_PASSWORD_ID = 0x00; public const byte LOGIN_FIELD_USERNAME_ID = 0x01; public const byte LOGIN_FIELD_EMAIL_ID = 0x02; diff --git a/src/KeyKeeper/PasswordStore/IPassStore.cs b/src/KeyKeeper/PasswordStore/IPassStore.cs index e7fbe14..fc7f1a5 100644 --- a/src/KeyKeeper/PasswordStore/IPassStore.cs +++ b/src/KeyKeeper/PasswordStore/IPassStore.cs @@ -1,3 +1,4 @@ +using System; using KeyKeeper.PasswordStore.Crypto; namespace KeyKeeper.PasswordStore; @@ -6,10 +7,15 @@ public interface IPassStore { bool Locked { get; } - IPassStoreDirectory GetRootDirectory(); - IPassStoreDirectory? GetGroupByType(byte groupType); + PassStoreEntryGroup GetRootDirectory(); + PassStoreEntryGroup? GetGroupByType(byte groupType); + public PassStoreEntry GetEntryById(Guid id); int GetTotalEntryCount(); void Unlock(CompositeKey key); void Lock(); void Save(); + + bool DeleteEntry(PassStoreEntryGroup? group, Guid id); + void AddEntry(PassStoreEntryGroup group, PassStoreEntry entry); + void UpdateEntry(PassStoreEntryGroup? group, Guid id, PassStoreEntry entry); } diff --git a/src/KeyKeeper/PasswordStore/IPassStoreDirectory.cs b/src/KeyKeeper/PasswordStore/IPassStoreDirectory.cs deleted file mode 100644 index 03996c6..0000000 --- a/src/KeyKeeper/PasswordStore/IPassStoreDirectory.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace KeyKeeper.PasswordStore; - -public interface IPassStoreDirectory : IEnumerable -{ - bool DeleteEntry(Guid id); - void AddEntry(PassStoreEntry entry); - void UpdateEntry(Guid id, PassStoreEntry entry); -} diff --git a/src/KeyKeeper/PasswordStore/PassStoreEntry.cs b/src/KeyKeeper/PasswordStore/PassStoreEntry.cs index 6a3858a..87f4ec4 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreEntry.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreEntry.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using static KeyKeeper.PasswordStore.FileFormatConstants; @@ -13,6 +14,7 @@ public abstract class PassStoreEntry public Guid IconType { get; set; } public string Name { get; set; } public PassStoreEntryType Type { get; set; } + public List Backlinks = new(); public string IconPath { get @@ -78,6 +80,9 @@ public abstract class PassStoreEntry } else if (entryType == ENTRY_PASS_ID) { return PassStoreEntryPassword.ReadFromStream(entryStream, id, createdAt, modifiedAt, iconType, name); + } else if (entryType == ENTRY_LINK_ID) + { + return PassStoreEntryLink.ReadFromStream(entryStream, id, createdAt, modifiedAt, iconType, name); } else { throw PassStoreFileException.InvalidPassStoreEntry; diff --git a/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs b/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs index c71523a..c5fa68b 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs @@ -7,7 +7,7 @@ using static KeyKeeper.PasswordStore.FileFormatConstants; namespace KeyKeeper.PasswordStore; -public class PassStoreEntryGroup : PassStoreEntry, IPassStoreDirectory +public class PassStoreEntryGroup : PassStoreEntry, IEnumerable { public byte GroupType { get; set; } public Guid? CustomGroupSubtype { get; set; } @@ -75,42 +75,6 @@ public class PassStoreEntryGroup : PassStoreEntry, IPassStoreDirectory } } - public void AddEntry(PassStoreEntry entry) - { - entry.Parent = this; - ChildEntries.Add(entry); - } - - public bool DeleteEntry(Guid id) - { - if (ChildEntries == null) - return false; - for (int i = 0; i < ChildEntries.Count; i++) - { - if (ChildEntries[i].Id == id) - { - ChildEntries.RemoveAt(i); - return true; - } - } - return false; - } - - public void UpdateEntry(Guid id, PassStoreEntry entry) - { - entry.Parent = this; - if (ChildEntries == null) - return; - for (int i = 0; i < ChildEntries.Count; i++) - { - if (ChildEntries[i].Id == id) - { - ChildEntries[i] = entry; - return; - } - } - } - IEnumerator IEnumerable.GetEnumerator() { return ChildEntries.GetEnumerator(); diff --git a/src/KeyKeeper/PasswordStore/PassStoreEntryLink.cs b/src/KeyKeeper/PasswordStore/PassStoreEntryLink.cs new file mode 100644 index 0000000..5a30a72 --- /dev/null +++ b/src/KeyKeeper/PasswordStore/PassStoreEntryLink.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using static KeyKeeper.PasswordStore.FileFormatConstants; + +namespace KeyKeeper.PasswordStore; + +public class PassStoreEntryLink : PassStoreEntry +{ + public PassStoreEntry? LinkTarget; + public Guid LinkTargetId; + + public override string DisplayName => LinkTarget?.DisplayName ?? Name; + + public PassStoreEntryLink(Guid id, DateTime createdAt, DateTime modifiedAt, + Guid targetId, PassStoreEntry? target = null) + { + Id = id; + CreationDate = createdAt; + ModificationDate = modifiedAt; + IconType = BuiltinEntryIconType.DEFAULT; + Name = ""; + LinkTargetId = targetId; + LinkTarget = target; + } + + public static PassStoreEntry ReadFromStream(Stream str, Guid id, DateTime createdAt, DateTime modifiedAt, Guid iconType, string name) + { + BinaryReader rd = new(str); + try + { + byte[] guidBuffer = new byte[16]; + if (rd.Read(guidBuffer) < 16) + throw PassStoreFileException.UnexpectedEndOfFile; + Guid linkTargetId = new(guidBuffer); + + return new PassStoreEntryLink(id, createdAt, modifiedAt, linkTargetId); + } catch (EndOfStreamException) + { + throw PassStoreFileException.UnexpectedEndOfFile; + } + } + + public override string ToString() + { + return string.Format( + "EntryLink (id={0} target_id={1} target={2})", + Id, LinkTargetId, LinkTarget); + } + + protected override byte[] InnerSerialize() + { + MemoryStream str = new(); + str.WriteByte(ENTRY_LINK_ID); + str.Write(LinkTargetId.ToByteArray()); + + return str.ToArray(); + } +} diff --git a/src/KeyKeeper/PasswordStore/PassStoreEntryType.cs b/src/KeyKeeper/PasswordStore/PassStoreEntryType.cs index 8be01f8..c47620f 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreEntryType.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreEntryType.cs @@ -4,4 +4,5 @@ public enum PassStoreEntryType { Password, Directory, + Link, } diff --git a/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs b/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs index 9820f2f..412afcb 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs @@ -14,20 +14,22 @@ namespace KeyKeeper.PasswordStore; /// public class PassStoreFileAccessor : IPassStore { - private const ushort FORMAT_VERSION_MAJOR = 0; - private const ushort FORMAT_VERSION_MINOR = 0; + private const ushort FORMAT_VERSION_MAJOR = 1; + private const ushort FORMAT_VERSION_MINOR = 1; private static readonly byte[] FORMAT_MAGIC = [0xf5, 0x3a, 0xa4, 0xb7, 0xeb, 0xd9, 0xc2, 0x12]; private string filename; private byte[]? key; private InnerEncryptionInfo? innerCrypto; private OuterEncryptionHeader? outerCryptoHdr; - private PassStoreEntry? root; + private PassStoreEntryGroup? root; + private Dictionary allEntries; public PassStoreFileAccessor(string filename, bool create, StoreCreationOptions? createOptions) { this.filename = filename; this.key = null; + this.allEntries = new(); if (!create) { CheckStoreFile(); @@ -42,14 +44,14 @@ public class PassStoreFileAccessor : IPassStore get { return key == null; } } - public IPassStoreDirectory GetRootDirectory() + public PassStoreEntryGroup GetRootDirectory() { if (Locked) throw new InvalidOperationException(); - return (IPassStoreDirectory)root!; + return root!; } - public IPassStoreDirectory? GetGroupByType(byte groupType) + public PassStoreEntryGroup? GetGroupByType(byte groupType) { if (Locked) throw new InvalidOperationException(); @@ -102,7 +104,9 @@ public class PassStoreFileAccessor : IPassStore case FILE_FIELD_CONFIG: break; case FILE_FIELD_STORE: - this.root = PassStoreEntry.ReadFromStream(cryptoReader); + root = PassStoreEntry.ReadFromStream(cryptoReader) as PassStoreEntryGroup; + AddEntriesToDict(root!); + ResolveLinks(); break; case FILE_FIELD_END: end = true; @@ -123,6 +127,8 @@ public class PassStoreFileAccessor : IPassStore Save(); Array.Fill(key!, 0); key = null; + root = null; + allEntries = new(); } public void Save() @@ -163,6 +169,96 @@ public class PassStoreFileAccessor : IPassStore file.SetLength(file.Position); } + public void AddEntry(PassStoreEntryGroup group, PassStoreEntry entry) + { + if (Locked) throw new InvalidOperationException("store locked"); + + entry.Parent = group; + group.ChildEntries.Add(entry); + if (entry is PassStoreEntryLink link) + { + link.LinkTarget ??= allEntries[link.LinkTargetId]; + if (link.LinkTarget == null) + throw new ArgumentException("invalid link target"); + PassStoreEntry t = link.LinkTarget; + if (!t.Backlinks.Contains(entry)) + t.Backlinks.Add(entry); + } + allEntries[entry.Id] = entry; + } + + public bool DeleteEntry(PassStoreEntryGroup? group, Guid id) + { + if (Locked) throw new InvalidOperationException("store locked"); + + if (group == null) + group = allEntries[id]?.Parent; + + if (group == null || group.ChildEntries == null) + return false; + + var ch = group.ChildEntries; + for (int i = 0; i < ch.Count; i++) + { + if (ch[i].Id == id) + { + for (int j = 0; j < ch[i].Backlinks.Count; j++) + { + PassStoreEntry bl = ch[i].Backlinks[j]; + if (bl is PassStoreEntryLink) + DeleteEntry(bl.Parent, bl.Id); + } + if (ch[i] is PassStoreEntryLink lnk && lnk.LinkTarget != null) + { + lnk.LinkTarget.Backlinks.Remove(lnk); + } + allEntries.Remove(ch[i].Id); + ch.RemoveAt(i); + return true; + } + } + return false; + } + + public void UpdateEntry(PassStoreEntryGroup? group, Guid id, PassStoreEntry entry) + { + if (Locked) throw new InvalidOperationException("store locked"); + + if (group == null) + group = allEntries[id]?.Parent; + + if (group == null || group.ChildEntries == null) + return; + + entry.Parent = group; + entry.Backlinks = allEntries[id].Backlinks; + foreach (PassStoreEntry bl in entry.Backlinks) + { + if (bl is PassStoreEntryLink lnk) + { + lnk.LinkTarget = entry; + } + } + + var ch = group.ChildEntries; + for (int i = 0; i < ch.Count; i++) + { + if (ch[i].Id == id) + { + allEntries.Remove(ch[i].Id); // убрать + allEntries[entry.Id] = entry; + ch[i] = entry; + return; + } + } + } + + public PassStoreEntry GetEntryById(Guid id) + { + if (Locked) throw new InvalidOperationException("store locked"); + return allEntries[id]; + } + /// /// Проверяет внешнюю целостность файла хранилища, то есть: /// 1. совпадает сигнатура (magic number) @@ -213,6 +309,30 @@ public class PassStoreFileAccessor : IPassStore OuterEncryptionUtil.CheckOuterEncryptionHeader(file); } + private void AddEntriesToDict(PassStoreEntryGroup root) + { + allEntries.Add(root.Id, root); + foreach (PassStoreEntry entry in root.ChildEntries) + { + if (entry is PassStoreEntryGroup group) + AddEntriesToDict(group); + else + allEntries.Add(entry.Id, entry); + } + } + + private void ResolveLinks() + { + foreach (var kv in allEntries) + { + if (kv.Value is PassStoreEntryLink lnk) + { + lnk.LinkTarget = allEntries[lnk.LinkTargetId]; + lnk.LinkTarget.Backlinks.Add(lnk); + } + } + } + private void CreateNewAndUnlock(StoreCreationOptions options) { using FileStream file = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None); @@ -257,9 +377,12 @@ public class PassStoreFileAccessor : IPassStore cryptoWriter.Flush(); cryptoWriter.Dispose(); + + AddEntriesToDict(root); + ResolveLinks(); } - private PassStoreEntry WriteInitialStoreTree(OuterEncryptionWriter w) + private PassStoreEntryGroup WriteInitialStoreTree(OuterEncryptionWriter w) { PassStoreEntryGroup defaultGroup = new( Guid.NewGuid(), diff --git a/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs b/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs index ec7e418..9b41c54 100644 --- a/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs +++ b/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs @@ -10,19 +10,18 @@ namespace KeyKeeper.ViewModels; public class UnlockedRepositoryViewModel : ViewModelBase { private IPassStore passStore; - private IPassStoreDirectory currentDirectory; + private PassStoreEntryGroup currentDirectory; private PassStoreEntryGroup? rootDirectory; private bool hasUnsavedChanges; private DispatcherTimer? _totpRefreshTimer; private Dictionary _totpCodes = new(); - public IEnumerable Passwords + public IEnumerable Passwords { get { return currentDirectory - .Where(entry => entry is PassStoreEntryPassword) - .Select(entry => (entry as PassStoreEntryPassword)!); + .Where(entry => entry is PassStoreEntryPassword || entry is PassStoreEntryLink lnk && lnk.LinkTarget is PassStoreEntryPassword); } } @@ -61,11 +60,11 @@ public class UnlockedRepositoryViewModel : ViewModelBase } } - public UnlockedRepositoryViewModel(IPassStore store, IPassStoreDirectory directory) + public UnlockedRepositoryViewModel(IPassStore store, PassStoreEntryGroup group) { passStore = store; - currentDirectory = directory; - rootDirectory = (directory as PassStoreEntryGroup)?.Parent; + currentDirectory = group; + rootDirectory = group.Parent; HasUnsavedChanges = false; InitializeTotpCodes(); StartTotpRefreshTimer(); @@ -89,7 +88,15 @@ public class UnlockedRepositoryViewModel : ViewModelBase { if (entry is PassStoreEntryPassword) { - currentDirectory.AddEntry(entry); + if (currentDirectory.GroupType == FileFormatConstants.GROUP_TYPE_DEFAULT) + { + passStore.AddEntry(currentDirectory, entry); + } else + { + // добавляем в All Passwords, но оставляем ссылку в текущей папке + passStore.AddEntry(passStore.GetGroupByType(FileFormatConstants.GROUP_TYPE_DEFAULT)!, entry); + passStore.AddEntry(currentDirectory, new PassStoreEntryLink(Guid.NewGuid(), DateTime.Now, DateTime.Now, entry.Id, entry)); + } HasUnsavedChanges = true; OnPropertyChanged(nameof(Passwords)); } @@ -99,21 +106,29 @@ public class UnlockedRepositoryViewModel : ViewModelBase { if (rootDirectory == null) return; - rootDirectory.AddEntry(group); + passStore.AddEntry(rootDirectory, group); HasUnsavedChanges = true; OnPropertyChanged(nameof(PasswordGroups)); } public void DeleteEntry(Guid id) { - currentDirectory.DeleteEntry(id); + PassStoreEntry entry = passStore.GetEntryById(id); + if (entry is PassStoreEntryLink lnk) + { + passStore.DeleteEntry(lnk.LinkTarget!.Parent, lnk.LinkTargetId); + passStore.DeleteEntry(currentDirectory, id); + } else + { + passStore.DeleteEntry(currentDirectory, id); + } HasUnsavedChanges = true; OnPropertyChanged(nameof(Passwords)); } public void UpdateEntry(PassStoreEntryPassword updatedEntry) { - currentDirectory.UpdateEntry(updatedEntry.Id, updatedEntry); + passStore.UpdateEntry(null, updatedEntry.Id, updatedEntry); HasUnsavedChanges = true; OnPropertyChanged(nameof(Passwords)); } @@ -124,6 +139,19 @@ public class UnlockedRepositoryViewModel : ViewModelBase HasUnsavedChanges = false; } + public static PassStoreEntryPassword? FollowLinkIfNeeded(PassStoreEntry entry) + { + if (entry is PassStoreEntryPassword passwordEntry) + { + return passwordEntry; + } + else if (entry is PassStoreEntryLink link && link.LinkTarget is PassStoreEntryPassword t) + { + return t; + } + return null; + } + private void ChangeDirectory(PassStoreEntryGroup newDir) { if (newDir == currentDirectory) @@ -140,7 +168,7 @@ public class UnlockedRepositoryViewModel : ViewModelBase private void InitializeTotpCodes() { _totpCodes.Clear(); - foreach (var entry in Passwords.Where(e => e.Totp != null)) + foreach (var entry in Passwords.Select(FollowLinkIfNeeded).Where(e => e?.Totp != null)) { _totpCodes[entry.Id] = TotpCodeGenerator.GenerateCode(entry.Totp!); } @@ -182,7 +210,7 @@ public class UnlockedRepositoryViewModel : ViewModelBase private int CalculateSecondsUntilNextTotpRefresh() { // Find the minimum seconds until next code change across all TOTP entries - var totpEntries = Passwords.Where(e => e.Totp != null).ToList(); + var totpEntries = Passwords.Select(FollowLinkIfNeeded).Where(e => e?.Totp != null).ToList(); if (totpEntries.Count == 0) return 60; // Default to 60 seconds if no TOTP entries diff --git a/src/KeyKeeper/Views/PasswordEntryConverter.cs b/src/KeyKeeper/Views/PasswordEntryConverter.cs new file mode 100644 index 0000000..6f44400 --- /dev/null +++ b/src/KeyKeeper/Views/PasswordEntryConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; +using KeyKeeper.PasswordStore; + +namespace KeyKeeper.Views; + +public class PasswordEntryConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + PassStoreEntry? entry = value as PassStoreEntry; + if (entry is PassStoreEntryLink link) + { + entry = link.LinkTarget; + } + + if (entry == null) + return value; + + if (parameter is string propertyPath && !string.IsNullOrEmpty(propertyPath)) + { + try + { + object? current = entry; + foreach (var prop in propertyPath.Split('.')) + { + if (current == null) + return null; + var propInfo = current.GetType().GetProperty(prop); + current = propInfo?.GetValue(current); + } + return current; + } + catch + { + return value; + } + } + + return entry; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return BindingNotification.UnsetValue; + } +} \ No newline at end of file diff --git a/src/KeyKeeper/Views/RepositoryWindow.axaml b/src/KeyKeeper/Views/RepositoryWindow.axaml index 960076d..35c7472 100644 --- a/src/KeyKeeper/Views/RepositoryWindow.axaml +++ b/src/KeyKeeper/Views/RepositoryWindow.axaml @@ -14,6 +14,7 @@ + @@ -130,11 +131,11 @@ Margin="10" HorizontalAlignment="Center"> - - + - @@ -146,7 +147,7 @@ Margin="0,4,0,0"> - + diff --git a/src/KeyKeeper/Views/RepositoryWindow.axaml.cs b/src/KeyKeeper/Views/RepositoryWindow.axaml.cs index 504c750..77196f0 100644 --- a/src/KeyKeeper/Views/RepositoryWindow.axaml.cs +++ b/src/KeyKeeper/Views/RepositoryWindow.axaml.cs @@ -118,6 +118,37 @@ public partial class RepositoryWindow : Window } } + private async void EditEntry(RepositoryWindowViewModel vm1, UnlockedRepositoryViewModel vm2, PassStoreEntry entry) + { + if (entry == null) + { + this.FindControlRecursive("NotificationHost")?.Show("No entry selected"); + return; + } + + EntryEditWindow dialog = new(); + + PassStoreEntry realEntry = entry; + if (realEntry is PassStoreEntryLink lnk) + realEntry = lnk.LinkTarget!; + + dialog.SetEntry((realEntry as PassStoreEntryPassword)!); + + vm1.StopLockTimer(); + + await dialog.ShowDialog(this); + + vm1.StartLockTimer(); + + if (dialog.EditedEntry != null) + { + if (entry is PassStoreEntryLink l) + l.LinkTarget = dialog.EditedEntry; + vm2.UpdateEntry(dialog.EditedEntry); + this.FindControlRecursive("NotificationHost")?.Show("Entry updated"); + } + } + private async void EditEntryButton_Click(object sender, RoutedEventArgs args) { if (DataContext is RepositoryWindowViewModel vm_ && vm_.CurrentPage is UnlockedRepositoryViewModel vm) @@ -129,27 +160,9 @@ public partial class RepositoryWindow : Window return; } - var selectedEntry = listBox.SelectedItem as PassStoreEntryPassword; - if (selectedEntry == null) - { - this.FindControlRecursive("NotificationHost")?.Show("No entry selected"); - return; - } - - EntryEditWindow dialog = new(); - dialog.SetEntry(selectedEntry); - - vm_.StopLockTimer(); - - await dialog.ShowDialog(this); - - vm_.StartLockTimer(); - - if (dialog.EditedEntry != null) - { - vm.UpdateEntry(dialog.EditedEntry); - this.FindControlRecursive("NotificationHost")?.Show("Entry updated"); - } + PassStoreEntry? entry = listBox.SelectedItem as PassStoreEntry; + if (entry == null) return; + EditEntry(vm_, vm, entry); } } @@ -212,39 +225,33 @@ public partial class RepositoryWindow : Window private async void EntryContextMenuItem_Click(object sender, RoutedEventArgs args) { - if (args.Source is StyledElement s && s.DataContext is PassStoreEntryPassword pwd) + if (args.Source is StyledElement s && s.DataContext is PassStoreEntry ent) { + PassStoreEntryPassword? pwd = UnlockedRepositoryViewModel.FollowLinkIfNeeded(ent); + if (pwd == null) return; + if (s.Name == "entryCtxMenuCopyUsername") { - Clipboard!.SetTextAsync(pwd.Username.Value); + await Clipboard!.SetTextAsync(pwd.Username.Value); this.FindControlRecursive("NotificationHost")?.Show("Username copied to clipboard"); } else if (s.Name == "entryCtxMenuCopyPassword") { - Clipboard!.SetTextAsync(pwd.Password.Value); + await Clipboard!.SetTextAsync(pwd.Password.Value); this.FindControlRecursive("NotificationHost")?.Show("Password copied to clipboard"); } else if (s.Name == "entryCtxMenuEdit") { if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm) { - EntryEditWindow dialog = new(); - dialog.SetEntry(pwd); - vm.StopLockTimer(); - await dialog.ShowDialog(this); - vm.StartLockTimer(); - if (dialog.EditedEntry != null) - { - pageVm.UpdateEntry(dialog.EditedEntry); - this.FindControlRecursive("NotificationHost")?.Show("Entry updated"); - } + EditEntry(vm, pageVm, ent); } } else if (s.Name == "entryCtxMenuDelete") { if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm) { - pageVm.DeleteEntry(pwd.Id); + pageVm.DeleteEntry(ent.Id); this.FindControlRecursive("NotificationHost")?.Show("Entry deleted"); } }