diff --git a/src/KeyKeeper/PasswordStore/EntryIconType.cs b/src/KeyKeeper/PasswordStore/EntryIconType.cs
index 0a1c7de..956f1c7 100644
--- a/src/KeyKeeper/PasswordStore/EntryIconType.cs
+++ b/src/KeyKeeper/PasswordStore/EntryIconType.cs
@@ -2,7 +2,7 @@ using System;
namespace KeyKeeper.PasswordStore;
-public static class EntryIconType
+public static class BuiltinEntryIconType
{
public static readonly Guid KEY = Guid.Parse("65ab3d55-1652-4f66-aac9-c3617f14e308");
public static readonly Guid DEFAULT = KEY;
diff --git a/src/KeyKeeper/ViewModels/EntryEditViewModel.cs b/src/KeyKeeper/ViewModels/EntryEditViewModel.cs
index a0a105a..55e8264 100644
--- a/src/KeyKeeper/ViewModels/EntryEditViewModel.cs
+++ b/src/KeyKeeper/ViewModels/EntryEditViewModel.cs
@@ -323,7 +323,6 @@ public class EntryEditViewModel : ViewModelBase
}
catch (Exception)
{
- // Validation should have caught this, but handle gracefully
totp = null;
}
}
@@ -332,7 +331,7 @@ public class EntryEditViewModel : ViewModelBase
id,
created,
DateTime.UtcNow,
- EntryIconType.DEFAULT,
+ BuiltinEntryIconType.DEFAULT,
EntryName.Trim(),
new LoginField() { Type = LOGIN_FIELD_USERNAME_ID, Value = Username.Trim() },
new LoginField() { Type = LOGIN_FIELD_PASSWORD_ID, Value = Password },
diff --git a/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs b/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs
index 7dd5e11..ec7e418 100644
--- a/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs
+++ b/src/KeyKeeper/ViewModels/UnlockedRepositoryViewModel.cs
@@ -95,6 +95,15 @@ public class UnlockedRepositoryViewModel : ViewModelBase
}
}
+ public void AddGroup(PassStoreEntryGroup group)
+ {
+ if (rootDirectory == null)
+ return;
+ rootDirectory.AddEntry(group);
+ HasUnsavedChanges = true;
+ OnPropertyChanged(nameof(PasswordGroups));
+ }
+
public void DeleteEntry(Guid id)
{
currentDirectory.DeleteEntry(id);
diff --git a/src/KeyKeeper/Views/CreateGroupDialog.axaml b/src/KeyKeeper/Views/CreateGroupDialog.axaml
new file mode 100644
index 0000000..988eaf7
--- /dev/null
+++ b/src/KeyKeeper/Views/CreateGroupDialog.axaml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/KeyKeeper/Views/CreateGroupDialog.axaml.cs b/src/KeyKeeper/Views/CreateGroupDialog.axaml.cs
new file mode 100644
index 0000000..ebbacee
--- /dev/null
+++ b/src/KeyKeeper/Views/CreateGroupDialog.axaml.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using KeyKeeper.PasswordStore;
+
+namespace KeyKeeper.Views;
+
+public partial class CreateGroupDialog : Window
+{
+ public string GroupName { get; private set; } = string.Empty;
+ public Guid IconType { get; private set; } = BuiltinEntryIconType.DEFAULT;
+ public bool Success { get; private set; }
+
+ private record IconChoice(Guid Id)
+ {
+ public string IconPath => $"avares://KeyKeeper/Assets/builtin-entry-icon-{Id}.svg";
+ }
+
+ public CreateGroupDialog()
+ {
+ InitializeComponent();
+
+ var icons = new List
+ {
+ new(BuiltinEntryIconType.KEY),
+ };
+ IconListBox.ItemsSource = icons;
+ IconListBox.SelectedIndex = 0;
+
+ NameTextBox.TextChanged += (_, _) => UpdateCreateButtonState();
+ KeyDown += OnKeyDown;
+ }
+
+ private void UpdateCreateButtonState()
+ {
+ CreateButton.IsEnabled = !string.IsNullOrWhiteSpace(NameTextBox.Text);
+ }
+
+ private void OnKeyDown(object? sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ Close();
+ else if (e.Key == Key.Enter && CreateButton.IsEnabled)
+ Submit();
+ }
+
+ private void CreateButton_Click(object? sender, RoutedEventArgs e) => Submit();
+
+ private void CancelButton_Click(object? sender, RoutedEventArgs e)
+ {
+ Success = false;
+ Close();
+ }
+
+ private void Submit()
+ {
+ var name = NameTextBox.Text?.Trim() ?? string.Empty;
+ if (string.IsNullOrEmpty(name))
+ {
+ ErrorText.Text = "Name cannot be empty";
+ ErrorText.IsVisible = true;
+ return;
+ }
+
+ GroupName = name;
+ if (IconListBox.SelectedItem is IconChoice choice)
+ IconType = choice.Id;
+
+ Success = true;
+ Close();
+ }
+}
diff --git a/src/KeyKeeper/Views/RepositoryWindow.axaml b/src/KeyKeeper/Views/RepositoryWindow.axaml
index 08cad88..960076d 100644
--- a/src/KeyKeeper/Views/RepositoryWindow.axaml
+++ b/src/KeyKeeper/Views/RepositoryWindow.axaml
@@ -96,6 +96,14 @@
Height="30"
HorizontalAlignment="Left"
Margin="0,20,0,0"/>
+
+
+
diff --git a/src/KeyKeeper/Views/RepositoryWindow.axaml.cs b/src/KeyKeeper/Views/RepositoryWindow.axaml.cs
index e3d7e08..504c750 100644
--- a/src/KeyKeeper/Views/RepositoryWindow.axaml.cs
+++ b/src/KeyKeeper/Views/RepositoryWindow.axaml.cs
@@ -153,6 +153,34 @@ public partial class RepositoryWindow : Window
}
}
+ private async void AddGroupButton_Click(object sender, RoutedEventArgs args)
+ {
+ if (DataContext is RepositoryWindowViewModel vm_ && vm_.CurrentPage is UnlockedRepositoryViewModel vm)
+ {
+ CreateGroupDialog dialog = new();
+
+ vm_.StopLockTimer();
+
+ await dialog.ShowDialog(this);
+
+ vm_.StartLockTimer();
+
+ if (dialog.Success)
+ {
+ var group = new PassStoreEntryGroup(
+ Guid.NewGuid(),
+ DateTime.UtcNow,
+ DateTime.UtcNow,
+ dialog.IconType,
+ dialog.GroupName,
+ FileFormatConstants.GROUP_TYPE_SIMPLE
+ );
+ vm.AddGroup(group);
+ this.FindControlRecursive("NotificationHost")?.Show("Group created");
+ }
+ }
+ }
+
private void SaveButton_Click(object sender, RoutedEventArgs args)
{
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)