From cc64f55d938248ff26f552136b3f615b102f0ba8 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Wed, 6 May 2026 23:48:57 +0300 Subject: [PATCH] change password group behavior 1. add several methods to IPassStore, including logic that was previously tied to groups 2. implement link entries - bidirectional linking between password entries and link entries, in order to mirror all entries in the default ("All Passwords") group 3. bump format version to 1.1 4. display links along with the real password entries 5. get rid of some repeated code --- .../PasswordStore/FileFormatConstants.cs | 1 + src/KeyKeeper/PasswordStore/IPassStore.cs | 10 +- .../PasswordStore/IPassStoreDirectory.cs | 11 -- src/KeyKeeper/PasswordStore/PassStoreEntry.cs | 5 + .../PasswordStore/PassStoreEntryGroup.cs | 38 +---- .../PasswordStore/PassStoreEntryLink.cs | 58 ++++++++ .../PasswordStore/PassStoreEntryType.cs | 1 + .../PasswordStore/PassStoreFileAccessor.cs | 139 +++++++++++++++++- .../ViewModels/UnlockedRepositoryViewModel.cs | 54 +++++-- src/KeyKeeper/Views/PasswordEntryConverter.cs | 49 ++++++ src/KeyKeeper/Views/RepositoryWindow.axaml | 9 +- src/KeyKeeper/Views/RepositoryWindow.axaml.cs | 77 +++++----- 12 files changed, 342 insertions(+), 110 deletions(-) delete mode 100644 src/KeyKeeper/PasswordStore/IPassStoreDirectory.cs create mode 100644 src/KeyKeeper/PasswordStore/PassStoreEntryLink.cs create mode 100644 src/KeyKeeper/Views/PasswordEntryConverter.cs 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"); } }