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
This commit is contained in:
2026-05-06 23:48:57 +03:00
parent 6ecabed730
commit cc64f55d93
12 changed files with 342 additions and 110 deletions

View File

@@ -19,6 +19,7 @@ static class FileFormatConstants
public const uint FILE_FIELD_END = 0x010ba81a; public const uint FILE_FIELD_END = 0x010ba81a;
public const byte ENTRY_PASS_ID = 0x00; public const byte ENTRY_PASS_ID = 0x00;
public const byte ENTRY_GROUP_ID = 0x01; 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_PASSWORD_ID = 0x00;
public const byte LOGIN_FIELD_USERNAME_ID = 0x01; public const byte LOGIN_FIELD_USERNAME_ID = 0x01;
public const byte LOGIN_FIELD_EMAIL_ID = 0x02; public const byte LOGIN_FIELD_EMAIL_ID = 0x02;

View File

@@ -1,3 +1,4 @@
using System;
using KeyKeeper.PasswordStore.Crypto; using KeyKeeper.PasswordStore.Crypto;
namespace KeyKeeper.PasswordStore; namespace KeyKeeper.PasswordStore;
@@ -6,10 +7,15 @@ public interface IPassStore
{ {
bool Locked { get; } bool Locked { get; }
IPassStoreDirectory GetRootDirectory(); PassStoreEntryGroup GetRootDirectory();
IPassStoreDirectory? GetGroupByType(byte groupType); PassStoreEntryGroup? GetGroupByType(byte groupType);
public PassStoreEntry GetEntryById(Guid id);
int GetTotalEntryCount(); int GetTotalEntryCount();
void Unlock(CompositeKey key); void Unlock(CompositeKey key);
void Lock(); void Lock();
void Save(); void Save();
bool DeleteEntry(PassStoreEntryGroup? group, Guid id);
void AddEntry(PassStoreEntryGroup group, PassStoreEntry entry);
void UpdateEntry(PassStoreEntryGroup? group, Guid id, PassStoreEntry entry);
} }

View File

@@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
namespace KeyKeeper.PasswordStore;
public interface IPassStoreDirectory : IEnumerable<PassStoreEntry>
{
bool DeleteEntry(Guid id);
void AddEntry(PassStoreEntry entry);
void UpdateEntry(Guid id, PassStoreEntry entry);
}

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using static KeyKeeper.PasswordStore.FileFormatConstants; using static KeyKeeper.PasswordStore.FileFormatConstants;
@@ -13,6 +14,7 @@ public abstract class PassStoreEntry
public Guid IconType { get; set; } public Guid IconType { get; set; }
public string Name { get; set; } public string Name { get; set; }
public PassStoreEntryType Type { get; set; } public PassStoreEntryType Type { get; set; }
public List<PassStoreEntry> Backlinks = new();
public string IconPath public string IconPath
{ {
get get
@@ -78,6 +80,9 @@ public abstract class PassStoreEntry
} else if (entryType == ENTRY_PASS_ID) } else if (entryType == ENTRY_PASS_ID)
{ {
return PassStoreEntryPassword.ReadFromStream(entryStream, id, createdAt, modifiedAt, iconType, name); 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 } else
{ {
throw PassStoreFileException.InvalidPassStoreEntry; throw PassStoreFileException.InvalidPassStoreEntry;

View File

@@ -7,7 +7,7 @@ using static KeyKeeper.PasswordStore.FileFormatConstants;
namespace KeyKeeper.PasswordStore; namespace KeyKeeper.PasswordStore;
public class PassStoreEntryGroup : PassStoreEntry, IPassStoreDirectory public class PassStoreEntryGroup : PassStoreEntry, IEnumerable<PassStoreEntry>
{ {
public byte GroupType { get; set; } public byte GroupType { get; set; }
public Guid? CustomGroupSubtype { 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<PassStoreEntry> IEnumerable<PassStoreEntry>.GetEnumerator() IEnumerator<PassStoreEntry> IEnumerable<PassStoreEntry>.GetEnumerator()
{ {
return ChildEntries.GetEnumerator(); return ChildEntries.GetEnumerator();

View File

@@ -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();
}
}

View File

@@ -4,4 +4,5 @@ public enum PassStoreEntryType
{ {
Password, Password,
Directory, Directory,
Link,
} }

View File

@@ -14,20 +14,22 @@ namespace KeyKeeper.PasswordStore;
/// </summary> /// </summary>
public class PassStoreFileAccessor : IPassStore public class PassStoreFileAccessor : IPassStore
{ {
private const ushort FORMAT_VERSION_MAJOR = 0; private const ushort FORMAT_VERSION_MAJOR = 1;
private const ushort FORMAT_VERSION_MINOR = 0; private const ushort FORMAT_VERSION_MINOR = 1;
private static readonly byte[] FORMAT_MAGIC = [0xf5, 0x3a, 0xa4, 0xb7, 0xeb, 0xd9, 0xc2, 0x12]; private static readonly byte[] FORMAT_MAGIC = [0xf5, 0x3a, 0xa4, 0xb7, 0xeb, 0xd9, 0xc2, 0x12];
private string filename; private string filename;
private byte[]? key; private byte[]? key;
private InnerEncryptionInfo? innerCrypto; private InnerEncryptionInfo? innerCrypto;
private OuterEncryptionHeader? outerCryptoHdr; private OuterEncryptionHeader? outerCryptoHdr;
private PassStoreEntry? root; private PassStoreEntryGroup? root;
private Dictionary<Guid, PassStoreEntry> allEntries;
public PassStoreFileAccessor(string filename, bool create, StoreCreationOptions? createOptions) public PassStoreFileAccessor(string filename, bool create, StoreCreationOptions? createOptions)
{ {
this.filename = filename; this.filename = filename;
this.key = null; this.key = null;
this.allEntries = new();
if (!create) if (!create)
{ {
CheckStoreFile(); CheckStoreFile();
@@ -42,14 +44,14 @@ public class PassStoreFileAccessor : IPassStore
get { return key == null; } get { return key == null; }
} }
public IPassStoreDirectory GetRootDirectory() public PassStoreEntryGroup GetRootDirectory()
{ {
if (Locked) if (Locked)
throw new InvalidOperationException(); throw new InvalidOperationException();
return (IPassStoreDirectory)root!; return root!;
} }
public IPassStoreDirectory? GetGroupByType(byte groupType) public PassStoreEntryGroup? GetGroupByType(byte groupType)
{ {
if (Locked) if (Locked)
throw new InvalidOperationException(); throw new InvalidOperationException();
@@ -102,7 +104,9 @@ public class PassStoreFileAccessor : IPassStore
case FILE_FIELD_CONFIG: case FILE_FIELD_CONFIG:
break; break;
case FILE_FIELD_STORE: case FILE_FIELD_STORE:
this.root = PassStoreEntry.ReadFromStream(cryptoReader); root = PassStoreEntry.ReadFromStream(cryptoReader) as PassStoreEntryGroup;
AddEntriesToDict(root!);
ResolveLinks();
break; break;
case FILE_FIELD_END: case FILE_FIELD_END:
end = true; end = true;
@@ -123,6 +127,8 @@ public class PassStoreFileAccessor : IPassStore
Save(); Save();
Array.Fill<byte>(key!, 0); Array.Fill<byte>(key!, 0);
key = null; key = null;
root = null;
allEntries = new();
} }
public void Save() public void Save()
@@ -163,6 +169,96 @@ public class PassStoreFileAccessor : IPassStore
file.SetLength(file.Position); 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];
}
/// <summary> /// <summary>
/// Проверяет внешнюю целостность файла хранилища, то есть: /// Проверяет внешнюю целостность файла хранилища, то есть:
/// 1. совпадает сигнатура (magic number) /// 1. совпадает сигнатура (magic number)
@@ -213,6 +309,30 @@ public class PassStoreFileAccessor : IPassStore
OuterEncryptionUtil.CheckOuterEncryptionHeader(file); 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) private void CreateNewAndUnlock(StoreCreationOptions options)
{ {
using FileStream file = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None); using FileStream file = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
@@ -257,9 +377,12 @@ public class PassStoreFileAccessor : IPassStore
cryptoWriter.Flush(); cryptoWriter.Flush();
cryptoWriter.Dispose(); cryptoWriter.Dispose();
AddEntriesToDict(root);
ResolveLinks();
} }
private PassStoreEntry WriteInitialStoreTree(OuterEncryptionWriter w) private PassStoreEntryGroup WriteInitialStoreTree(OuterEncryptionWriter w)
{ {
PassStoreEntryGroup defaultGroup = new( PassStoreEntryGroup defaultGroup = new(
Guid.NewGuid(), Guid.NewGuid(),

View File

@@ -10,19 +10,18 @@ namespace KeyKeeper.ViewModels;
public class UnlockedRepositoryViewModel : ViewModelBase public class UnlockedRepositoryViewModel : ViewModelBase
{ {
private IPassStore passStore; private IPassStore passStore;
private IPassStoreDirectory currentDirectory; private PassStoreEntryGroup currentDirectory;
private PassStoreEntryGroup? rootDirectory; private PassStoreEntryGroup? rootDirectory;
private bool hasUnsavedChanges; private bool hasUnsavedChanges;
private DispatcherTimer? _totpRefreshTimer; private DispatcherTimer? _totpRefreshTimer;
private Dictionary<Guid, string> _totpCodes = new(); private Dictionary<Guid, string> _totpCodes = new();
public IEnumerable<PassStoreEntryPassword> Passwords public IEnumerable<PassStoreEntry> Passwords
{ {
get get
{ {
return currentDirectory return currentDirectory
.Where(entry => entry is PassStoreEntryPassword) .Where(entry => entry is PassStoreEntryPassword || entry is PassStoreEntryLink lnk && lnk.LinkTarget is PassStoreEntryPassword);
.Select(entry => (entry as PassStoreEntryPassword)!);
} }
} }
@@ -61,11 +60,11 @@ public class UnlockedRepositoryViewModel : ViewModelBase
} }
} }
public UnlockedRepositoryViewModel(IPassStore store, IPassStoreDirectory directory) public UnlockedRepositoryViewModel(IPassStore store, PassStoreEntryGroup group)
{ {
passStore = store; passStore = store;
currentDirectory = directory; currentDirectory = group;
rootDirectory = (directory as PassStoreEntryGroup)?.Parent; rootDirectory = group.Parent;
HasUnsavedChanges = false; HasUnsavedChanges = false;
InitializeTotpCodes(); InitializeTotpCodes();
StartTotpRefreshTimer(); StartTotpRefreshTimer();
@@ -89,7 +88,15 @@ public class UnlockedRepositoryViewModel : ViewModelBase
{ {
if (entry is PassStoreEntryPassword) 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; HasUnsavedChanges = true;
OnPropertyChanged(nameof(Passwords)); OnPropertyChanged(nameof(Passwords));
} }
@@ -99,21 +106,29 @@ public class UnlockedRepositoryViewModel : ViewModelBase
{ {
if (rootDirectory == null) if (rootDirectory == null)
return; return;
rootDirectory.AddEntry(group); passStore.AddEntry(rootDirectory, group);
HasUnsavedChanges = true; HasUnsavedChanges = true;
OnPropertyChanged(nameof(PasswordGroups)); OnPropertyChanged(nameof(PasswordGroups));
} }
public void DeleteEntry(Guid id) 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; HasUnsavedChanges = true;
OnPropertyChanged(nameof(Passwords)); OnPropertyChanged(nameof(Passwords));
} }
public void UpdateEntry(PassStoreEntryPassword updatedEntry) public void UpdateEntry(PassStoreEntryPassword updatedEntry)
{ {
currentDirectory.UpdateEntry(updatedEntry.Id, updatedEntry); passStore.UpdateEntry(null, updatedEntry.Id, updatedEntry);
HasUnsavedChanges = true; HasUnsavedChanges = true;
OnPropertyChanged(nameof(Passwords)); OnPropertyChanged(nameof(Passwords));
} }
@@ -124,6 +139,19 @@ public class UnlockedRepositoryViewModel : ViewModelBase
HasUnsavedChanges = false; 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) private void ChangeDirectory(PassStoreEntryGroup newDir)
{ {
if (newDir == currentDirectory) if (newDir == currentDirectory)
@@ -140,7 +168,7 @@ public class UnlockedRepositoryViewModel : ViewModelBase
private void InitializeTotpCodes() private void InitializeTotpCodes()
{ {
_totpCodes.Clear(); _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!); _totpCodes[entry.Id] = TotpCodeGenerator.GenerateCode(entry.Totp!);
} }
@@ -182,7 +210,7 @@ public class UnlockedRepositoryViewModel : ViewModelBase
private int CalculateSecondsUntilNextTotpRefresh() private int CalculateSecondsUntilNextTotpRefresh()
{ {
// Find the minimum seconds until next code change across all TOTP entries // 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) if (totpEntries.Count == 0)
return 60; // Default to 60 seconds if no TOTP entries return 60; // Default to 60 seconds if no TOTP entries

View File

@@ -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;
}
}

View File

@@ -14,6 +14,7 @@
<Window.Resources> <Window.Resources>
<kkp:TotpCodeConverter x:Key="TotpCodeConverter" /> <kkp:TotpCodeConverter x:Key="TotpCodeConverter" />
<kkp:PasswordEntryConverter x:Key="PasswordEntryConverter" />
</Window.Resources> </Window.Resources>
<Window.DataTemplates> <Window.DataTemplates>
@@ -130,11 +131,11 @@
Margin="10" Margin="10"
HorizontalAlignment="Center"> HorizontalAlignment="Center">
<Svg Path="{Binding IconPath}" Width="48" Height="48"/> <Svg Path="{Binding ., Converter={StaticResource PasswordEntryConverter}, ConverterParameter=IconPath}" Width="48" Height="48"/>
<TextBlock Text="{Binding Name}" <TextBlock Text="{Binding ., Converter={StaticResource PasswordEntryConverter}, ConverterParameter=Name}"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Foreground="Black" /> Foreground="Black" />
<TextBlock Text="{Binding Username.Value}" <TextBlock Text="{Binding ., Converter={StaticResource PasswordEntryConverter}, ConverterParameter=Username.Value}"
Foreground="#666" Foreground="#666"
HorizontalAlignment="Center" /> HorizontalAlignment="Center" />
@@ -146,7 +147,7 @@
Margin="0,4,0,0"> Margin="0,4,0,0">
<TextBlock.Text> <TextBlock.Text>
<MultiBinding Converter="{StaticResource TotpCodeConverter}"> <MultiBinding Converter="{StaticResource TotpCodeConverter}">
<Binding /> <Binding Converter="{StaticResource PasswordEntryConverter}" />
<Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ListBox}" /> <Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ListBox}" />
</MultiBinding> </MultiBinding>
</TextBlock.Text> </TextBlock.Text>

View File

@@ -118,6 +118,37 @@ public partial class RepositoryWindow : Window
} }
} }
private async void EditEntry(RepositoryWindowViewModel vm1, UnlockedRepositoryViewModel vm2, PassStoreEntry entry)
{
if (entry == null)
{
this.FindControlRecursive<ToastNotificationHost>("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<ToastNotificationHost>("NotificationHost")?.Show("Entry updated");
}
}
private async void EditEntryButton_Click(object sender, RoutedEventArgs args) private async void EditEntryButton_Click(object sender, RoutedEventArgs args)
{ {
if (DataContext is RepositoryWindowViewModel vm_ && vm_.CurrentPage is UnlockedRepositoryViewModel vm) if (DataContext is RepositoryWindowViewModel vm_ && vm_.CurrentPage is UnlockedRepositoryViewModel vm)
@@ -129,27 +160,9 @@ public partial class RepositoryWindow : Window
return; return;
} }
var selectedEntry = listBox.SelectedItem as PassStoreEntryPassword; PassStoreEntry? entry = listBox.SelectedItem as PassStoreEntry;
if (selectedEntry == null) if (entry == null) return;
{ EditEntry(vm_, vm, entry);
this.FindControlRecursive<ToastNotificationHost>("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<ToastNotificationHost>("NotificationHost")?.Show("Entry updated");
}
} }
} }
@@ -212,39 +225,33 @@ public partial class RepositoryWindow : Window
private async void EntryContextMenuItem_Click(object sender, RoutedEventArgs args) 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") if (s.Name == "entryCtxMenuCopyUsername")
{ {
Clipboard!.SetTextAsync(pwd.Username.Value); await Clipboard!.SetTextAsync(pwd.Username.Value);
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Username copied to clipboard"); this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Username copied to clipboard");
} }
else if (s.Name == "entryCtxMenuCopyPassword") else if (s.Name == "entryCtxMenuCopyPassword")
{ {
Clipboard!.SetTextAsync(pwd.Password.Value); await 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") else if (s.Name == "entryCtxMenuEdit")
{ {
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm) if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
{ {
EntryEditWindow dialog = new(); EditEntry(vm, pageVm, ent);
dialog.SetEntry(pwd);
vm.StopLockTimer();
await dialog.ShowDialog(this);
vm.StartLockTimer();
if (dialog.EditedEntry != null)
{
pageVm.UpdateEntry(dialog.EditedEntry);
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Entry updated");
}
} }
} }
else if (s.Name == "entryCtxMenuDelete") else if (s.Name == "entryCtxMenuDelete")
{ {
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm) if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
{ {
pageVm.DeleteEntry(pwd.Id); pageVm.DeleteEntry(ent.Id);
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Entry deleted"); this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Entry deleted");
} }
} }