mirror of
https://github.com/KeyKeeperApp/KeyKeeper.git
synced 2026-05-18 22:36:30 +03:00
add context menu for groups
This commit is contained in:
5
src/KeyKeeper/Models/GroupData.cs
Normal file
5
src/KeyKeeper/Models/GroupData.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace KeyKeeper.Models;
|
||||
|
||||
public record GroupEditData(string Name, Guid IconType);
|
||||
@@ -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();
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Spacing="8"
|
||||
Margin="0,16,0,0">
|
||||
<Button Content="Save"
|
||||
|
||||
38
src/KeyKeeper/Views/ConfirmationDialog.axaml
Normal file
38
src/KeyKeeper/Views/ConfirmationDialog.axaml
Normal 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>
|
||||
40
src/KeyKeeper/Views/ConfirmationDialog.axaml.cs
Normal file
40
src/KeyKeeper/Views/ConfirmationDialog.axaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user