add context menu for groups

This commit is contained in:
2026-05-07 23:26:00 +03:00
parent d1ad7df811
commit 4e53b710a5
9 changed files with 219 additions and 18 deletions

View File

@@ -0,0 +1,5 @@
using System;
namespace KeyKeeper.Models;
public record GroupEditData(string Name, Guid IconType);

View File

@@ -160,6 +160,29 @@ public class UnlockedRepositoryViewModel : ViewModelBase
OnPropertyChanged(nameof(Passwords));
}
public void UpdateGroup(PassStoreEntryGroup group, string newName, Guid newIconType)
{
group.Name = newName;
group.IconType = newIconType;
group.ModificationDate = DateTime.UtcNow;
HasUnsavedChanges = true;
OnPropertyChanged(nameof(PasswordGroups));
}
public void DeleteGroup(PassStoreEntryGroup group)
{
if (rootDirectory == null)
return;
passStore.DeleteEntry(rootDirectory, group.Id);
if (currentDirectory == group && rootDirectory != null)
{
ChangeDirectory(rootDirectory.ChildGroups.FirstOrDefault() ?? rootDirectory);
}
HasUnsavedChanges = true;
OnPropertyChanged(nameof(PasswordGroups));
}
public void Save()
{
passStore.Save();

View File

@@ -16,6 +16,7 @@
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Spacing="8"
Margin="0,16,0,0">
<Button Content="Save"

View File

@@ -0,0 +1,38 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="KeyKeeper.Views.ConfirmationDialog"
Title="Confirm Action"
Background="White"
Icon="/Assets/icon.ico"
Width="380" Height="180"
WindowStartupLocation="CenterOwner"
CanResize="False">
<Grid Margin="20" RowDefinitions="*,*,Auto">
<TextBlock Grid.Row="0"
x:Name="TitleText"
FontSize="18"
FontWeight="Bold"
Foreground="#2328C4"
TextWrapping="Wrap"/>
<TextBlock Grid.Row="1"
x:Name="MessageText"
FontSize="14"
Foreground="Black"
TextWrapping="Wrap"/>
<StackPanel Grid.Row="2" Orientation="Horizontal"
HorizontalAlignment="Right" Spacing="10" VerticalAlignment="Bottom">
<Button Content="Cancel"
Width="80"
Click="CancelButton_Click"/>
<Button x:Name="ConfirmButton"
Content="Confirm"
Width="80"
Click="ConfirmButton_Click"/>
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,40 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace KeyKeeper.Views;
public partial class ConfirmationDialog : Window
{
public bool Confirmed { get; private set; }
public ConfirmationDialog()
{
InitializeComponent();
}
public void SetContent(string title, string message)
{
TitleText.Text = title;
MessageText.Text = message;
ConfirmButton.Content = "Confirm";
}
public void SetContent(string title, string message, string confirmButtonText)
{
TitleText.Text = title;
MessageText.Text = message;
ConfirmButton.Content = confirmButtonText;
}
private void CancelButton_Click(object? sender, RoutedEventArgs e)
{
Confirmed = false;
Close();
}
private void ConfirmButton_Click(object? sender, RoutedEventArgs e)
{
Confirmed = true;
Close();
}
}

View File

@@ -9,7 +9,8 @@
CanResize="False">
<StackPanel Margin="20" Spacing="16">
<TextBlock Text="Create new group"
<TextBlock x:Name="TitleText"
Text="Create new group"
FontSize="20"
FontWeight="Bold"
Foreground="#2328C4"/>
@@ -50,11 +51,11 @@
<Button Content="Cancel"
Width="80"
Click="CancelButton_Click"/>
<Button x:Name="CreateButton"
<Button x:Name="ActionButton"
Content="Create"
Width="80"
IsEnabled="False"
Click="CreateButton_Click"/>
Click="ActionButton_Click"/>
</StackPanel>
</StackPanel>
</Window>

View File

@@ -3,15 +3,14 @@ using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using KeyKeeper.Models;
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; }
public GroupEditData? GroupData { get; private set; }
private record IconChoice(Guid Id)
{
@@ -29,28 +28,44 @@ public partial class CreateGroupDialog : Window
IconListBox.ItemsSource = icons;
IconListBox.SelectedIndex = 0;
NameTextBox.TextChanged += (_, _) => UpdateCreateButtonState();
NameTextBox.TextChanged += (_, _) => UpdateActionButtonState();
KeyDown += OnKeyDown;
}
private void UpdateCreateButtonState()
public void SetupForEdit(PassStoreEntryGroup group)
{
CreateButton.IsEnabled = !string.IsNullOrWhiteSpace(NameTextBox.Text);
TitleText.Text = "Edit group";
ActionButton.Content = "Save";
NameTextBox.Text = group.Name;
for (int i = 0; i < IconListBox.ItemCount; i++)
{
if (IconListBox.Items[i] is IconChoice choice && choice.Id == group.IconType)
{
IconListBox.SelectedIndex = i;
break;
}
}
}
private void UpdateActionButtonState()
{
ActionButton.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)
else if (e.Key == Key.Enter && ActionButton.IsEnabled)
Submit();
}
private void CreateButton_Click(object? sender, RoutedEventArgs e) => Submit();
private void ActionButton_Click(object? sender, RoutedEventArgs e) => Submit();
private void CancelButton_Click(object? sender, RoutedEventArgs e)
{
Success = false;
GroupData = null;
Close();
}
@@ -64,11 +79,11 @@ public partial class CreateGroupDialog : Window
return;
}
GroupName = name;
var iconType = BuiltinEntryIconType.DEFAULT;
if (IconListBox.SelectedItem is IconChoice choice)
IconType = choice.Id;
iconType = choice.Id;
Success = true;
GroupData = new GroupEditData(name, iconType);
Close();
}
}

View File

@@ -69,6 +69,12 @@
DoubleTapped="Entry_DoubleTapped">
<TextBlock Text="{Binding DisplayName}"
Foreground="Black" />
<Border.ContextMenu>
<ContextMenu>
<MenuItem Name="groupCtxMenuEdit" Header="Edit" Click="GroupContextMenuItem_Click" />
<MenuItem Name="groupCtxMenuDelete" Header="Delete" Click="GroupContextMenuItem_Click" />
</ContextMenu>
</Border.ContextMenu>
</Border>
</TreeDataTemplate>
</TreeView.ItemTemplate>

View File

@@ -1,10 +1,12 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Input;
using KeyKeeper.PasswordStore;
using KeyKeeper.Models;
using KeyKeeper.ViewModels;
using Avalonia.VisualTree;
using Avalonia.Controls.Presenters;
@@ -180,14 +182,14 @@ public partial class RepositoryWindow : Window
vm_.StartLockTimer();
if (dialog.Success)
if (dialog.GroupData != null)
{
var group = new PassStoreEntryGroup(
Guid.NewGuid(),
DateTime.UtcNow,
DateTime.UtcNow,
dialog.IconType,
dialog.GroupName,
dialog.GroupData.IconType,
dialog.GroupData.Name,
FileFormatConstants.GROUP_TYPE_SIMPLE
);
vm.AddGroup(group);
@@ -304,6 +306,76 @@ public partial class RepositoryWindow : Window
_contextMenuEntry = null;
}
private async void GroupContextMenuItem_Click(object sender, RoutedEventArgs args)
{
if (args.Source is not StyledElement s || s.DataContext is not PassStoreEntryGroup group)
return;
if (DataContext is not RepositoryWindowViewModel vm ||
vm.CurrentPage is not UnlockedRepositoryViewModel pageVm)
return;
if (group.GroupType == FileFormatConstants.GROUP_TYPE_DEFAULT ||
group.GroupType == FileFormatConstants.GROUP_TYPE_FAVOURITES ||
group.GroupType == FileFormatConstants.GROUP_TYPE_ROOT)
return;
if (s.Name == "groupCtxMenuEdit")
{
await EditGroup(vm, pageVm, group);
}
else if (s.Name == "groupCtxMenuDelete")
{
await DeleteGroup(group);
}
}
private async Task EditGroup(RepositoryWindowViewModel vm, UnlockedRepositoryViewModel pageVm, PassStoreEntryGroup group)
{
CreateGroupDialog dialog = new();
dialog.SetupForEdit(group);
vm.StopLockTimer();
await dialog.ShowDialog(this);
vm.StartLockTimer();
if (dialog.GroupData != null)
{
pageVm.UpdateGroup(group, dialog.GroupData.Name, dialog.GroupData.IconType);
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Group updated");
}
}
private async Task DeleteGroup(PassStoreEntryGroup group)
{
ConfirmationDialog confirmDialog = new();
confirmDialog.SetContent(
"Delete Group",
$"Are you sure you want to delete the group '{group.DisplayName}'? This action cannot be undone.",
"Delete"
);
if (DataContext is not RepositoryWindowViewModel vm)
return;
vm.StopLockTimer();
await confirmDialog.ShowDialog(this);
vm.StartLockTimer();
if (confirmDialog.Confirmed)
{
if (vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
{
pageVm.DeleteGroup(group);
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Group deleted");
}
}
}
private async void EntryContextMenuItem_Click(object sender, RoutedEventArgs args)
{
if (args.Source is StyledElement s && s.DataContext is PassStoreEntry ent)