From 69934338f230fb089b61755ce184d219902b7f89 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Sun, 3 May 2026 15:24:37 +0300 Subject: [PATCH 1/9] add group tree display --- src/KeyKeeper/PasswordStore/PassStoreEntry.cs | 1 + .../PasswordStore/PassStoreEntryGroup.cs | 11 +++++ .../PasswordStore/PassStoreFileAccessor.cs | 12 ++++- .../ViewModels/UnlockedRepositoryViewModel.cs | 44 +++++++++++++++++++ src/KeyKeeper/Views/RepositoryWindow.axaml | 31 ++++++------- 5 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/KeyKeeper/PasswordStore/PassStoreEntry.cs b/src/KeyKeeper/PasswordStore/PassStoreEntry.cs index a9e6a79..6a3858a 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreEntry.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreEntry.cs @@ -20,6 +20,7 @@ public abstract class PassStoreEntry return $"avares://KeyKeeper/Assets/builtin-entry-icon-{IconType}.svg"; } } + public virtual string DisplayName => Name; public void WriteToStream(Stream str) { diff --git a/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs b/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs index 53ebd77..c71523a 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreEntryGroup.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using static KeyKeeper.PasswordStore.FileFormatConstants; namespace KeyKeeper.PasswordStore; @@ -12,6 +13,16 @@ public class PassStoreEntryGroup : PassStoreEntry, IPassStoreDirectory public Guid? CustomGroupSubtype { get; set; } public List ChildEntries { get; set; } + public override string DisplayName => GroupType switch + { + GROUP_TYPE_DEFAULT => "All Passwords", + GROUP_TYPE_FAVOURITES => "Favourites", + GROUP_TYPE_ROOT => ":root:", + _ => Name + }; + + public IEnumerable ChildGroups => ChildEntries.OfType(); + public PassStoreEntryGroup(Guid id, DateTime createdAt, DateTime modifiedAt, Guid iconType, string name, byte groupType, List? children = null, diff --git a/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs b/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs index 7b8f471..9820f2f 100644 --- a/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs +++ b/src/KeyKeeper/PasswordStore/PassStoreFileAccessor.cs @@ -269,6 +269,15 @@ public class PassStoreFileAccessor : IPassStore "", GROUP_TYPE_DEFAULT ); + PassStoreEntryGroup favourites = new( + Guid.NewGuid(), + DateTime.UtcNow, + DateTime.UtcNow, + Guid.Empty, + "", + GROUP_TYPE_FAVOURITES + ); + PassStoreEntryGroup root = new( Guid.NewGuid(), DateTime.UtcNow, @@ -276,9 +285,10 @@ public class PassStoreFileAccessor : IPassStore Guid.Empty, "", GROUP_TYPE_ROOT, - [defaultGroup] + [defaultGroup, favourites] ); defaultGroup.Parent = root; + favourites.Parent = root; root.WriteToStream(w); return root; } diff --git a/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs b/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs index 444cdac..7dd5e11 100644 --- a/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs +++ b/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs @@ -11,6 +11,7 @@ public class UnlockedRepositoryViewModel : ViewModelBase { private IPassStore passStore; private IPassStoreDirectory currentDirectory; + private PassStoreEntryGroup? rootDirectory; private bool hasUnsavedChanges; private DispatcherTimer? _totpRefreshTimer; private Dictionary _totpCodes = new(); @@ -25,6 +26,31 @@ public class UnlockedRepositoryViewModel : ViewModelBase } } + public IEnumerable PasswordGroups + { + get + { + if (rootDirectory == null) return []; + return rootDirectory + .Where(entry => entry is PassStoreEntryGroup) + .Select(entry => (entry as PassStoreEntryGroup)!); + } + } + public PassStoreEntryGroup SelectedPasswordGroup + { + get + { + return PasswordGroups.First(group => group == currentDirectory); + } + set + { + if (PasswordGroups.Any(group => group == value)) + { + ChangeDirectory(value); + } + } + } + public bool HasUnsavedChanges { get => hasUnsavedChanges; @@ -39,6 +65,7 @@ public class UnlockedRepositoryViewModel : ViewModelBase { passStore = store; currentDirectory = directory; + rootDirectory = (directory as PassStoreEntryGroup)?.Parent; HasUnsavedChanges = false; InitializeTotpCodes(); StartTotpRefreshTimer(); @@ -88,6 +115,19 @@ public class UnlockedRepositoryViewModel : ViewModelBase HasUnsavedChanges = false; } + private void ChangeDirectory(PassStoreEntryGroup newDir) + { + if (newDir == currentDirectory) + return; + + currentDirectory = newDir; + InitializeTotpCodes(); + StartTotpRefreshTimer(); + + OnPropertyChanged(nameof(SelectedPasswordGroup)); + OnPropertyChanged(nameof(Passwords)); + } + private void InitializeTotpCodes() { _totpCodes.Clear(); @@ -102,6 +142,10 @@ public class UnlockedRepositoryViewModel : ViewModelBase // Calculate time until next TOTP period boundary int secondsUntilNextCode = CalculateSecondsUntilNextTotpRefresh(); + if (_totpRefreshTimer != null) + { + _totpRefreshTimer.Stop(); + } _totpRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(secondsUntilNextCode) diff --git a/src/KeyKeeper/Views/RepositoryWindow.axaml b/src/KeyKeeper/Views/RepositoryWindow.axaml index 6378b15..08cad88 100644 --- a/src/KeyKeeper/Views/RepositoryWindow.axaml +++ b/src/KeyKeeper/Views/RepositoryWindow.axaml @@ -56,22 +56,23 @@ - - + + + + + + + + + - -