mirror of
https://github.com/KeyKeeperApp/KeyKeeper.git
synced 2026-05-19 06:46:32 +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));
|
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()
|
public void Save()
|
||||||
{
|
{
|
||||||
passStore.Save();
|
passStore.Save();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<StackPanel Grid.Row="1"
|
<StackPanel Grid.Row="1"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
Spacing="8"
|
Spacing="8"
|
||||||
Margin="0,16,0,0">
|
Margin="0,16,0,0">
|
||||||
<Button Content="Save"
|
<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">
|
CanResize="False">
|
||||||
|
|
||||||
<StackPanel Margin="20" Spacing="16">
|
<StackPanel Margin="20" Spacing="16">
|
||||||
<TextBlock Text="Create new group"
|
<TextBlock x:Name="TitleText"
|
||||||
|
Text="Create new group"
|
||||||
FontSize="20"
|
FontSize="20"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
Foreground="#2328C4"/>
|
Foreground="#2328C4"/>
|
||||||
@@ -50,11 +51,11 @@
|
|||||||
<Button Content="Cancel"
|
<Button Content="Cancel"
|
||||||
Width="80"
|
Width="80"
|
||||||
Click="CancelButton_Click"/>
|
Click="CancelButton_Click"/>
|
||||||
<Button x:Name="CreateButton"
|
<Button x:Name="ActionButton"
|
||||||
Content="Create"
|
Content="Create"
|
||||||
Width="80"
|
Width="80"
|
||||||
IsEnabled="False"
|
IsEnabled="False"
|
||||||
Click="CreateButton_Click"/>
|
Click="ActionButton_Click"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ using System.Collections.Generic;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
|
using KeyKeeper.Models;
|
||||||
using KeyKeeper.PasswordStore;
|
using KeyKeeper.PasswordStore;
|
||||||
|
|
||||||
namespace KeyKeeper.Views;
|
namespace KeyKeeper.Views;
|
||||||
|
|
||||||
public partial class CreateGroupDialog : Window
|
public partial class CreateGroupDialog : Window
|
||||||
{
|
{
|
||||||
public string GroupName { get; private set; } = string.Empty;
|
public GroupEditData? GroupData { get; private set; }
|
||||||
public Guid IconType { get; private set; } = BuiltinEntryIconType.DEFAULT;
|
|
||||||
public bool Success { get; private set; }
|
|
||||||
|
|
||||||
private record IconChoice(Guid Id)
|
private record IconChoice(Guid Id)
|
||||||
{
|
{
|
||||||
@@ -29,28 +28,44 @@ public partial class CreateGroupDialog : Window
|
|||||||
IconListBox.ItemsSource = icons;
|
IconListBox.ItemsSource = icons;
|
||||||
IconListBox.SelectedIndex = 0;
|
IconListBox.SelectedIndex = 0;
|
||||||
|
|
||||||
NameTextBox.TextChanged += (_, _) => UpdateCreateButtonState();
|
NameTextBox.TextChanged += (_, _) => UpdateActionButtonState();
|
||||||
KeyDown += OnKeyDown;
|
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)
|
private void OnKeyDown(object? sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key == Key.Escape)
|
if (e.Key == Key.Escape)
|
||||||
Close();
|
Close();
|
||||||
else if (e.Key == Key.Enter && CreateButton.IsEnabled)
|
else if (e.Key == Key.Enter && ActionButton.IsEnabled)
|
||||||
Submit();
|
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)
|
private void CancelButton_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
Success = false;
|
GroupData = null;
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,11 +79,11 @@ public partial class CreateGroupDialog : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupName = name;
|
var iconType = BuiltinEntryIconType.DEFAULT;
|
||||||
if (IconListBox.SelectedItem is IconChoice choice)
|
if (IconListBox.SelectedItem is IconChoice choice)
|
||||||
IconType = choice.Id;
|
iconType = choice.Id;
|
||||||
|
|
||||||
Success = true;
|
GroupData = new GroupEditData(name, iconType);
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,12 @@
|
|||||||
DoubleTapped="Entry_DoubleTapped">
|
DoubleTapped="Entry_DoubleTapped">
|
||||||
<TextBlock Text="{Binding DisplayName}"
|
<TextBlock Text="{Binding DisplayName}"
|
||||||
Foreground="Black" />
|
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>
|
</Border>
|
||||||
</TreeDataTemplate>
|
</TreeDataTemplate>
|
||||||
</TreeView.ItemTemplate>
|
</TreeView.ItemTemplate>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using KeyKeeper.PasswordStore;
|
using KeyKeeper.PasswordStore;
|
||||||
|
using KeyKeeper.Models;
|
||||||
using KeyKeeper.ViewModels;
|
using KeyKeeper.ViewModels;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
using Avalonia.Controls.Presenters;
|
using Avalonia.Controls.Presenters;
|
||||||
@@ -180,14 +182,14 @@ public partial class RepositoryWindow : Window
|
|||||||
|
|
||||||
vm_.StartLockTimer();
|
vm_.StartLockTimer();
|
||||||
|
|
||||||
if (dialog.Success)
|
if (dialog.GroupData != null)
|
||||||
{
|
{
|
||||||
var group = new PassStoreEntryGroup(
|
var group = new PassStoreEntryGroup(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
DateTime.UtcNow,
|
DateTime.UtcNow,
|
||||||
DateTime.UtcNow,
|
DateTime.UtcNow,
|
||||||
dialog.IconType,
|
dialog.GroupData.IconType,
|
||||||
dialog.GroupName,
|
dialog.GroupData.Name,
|
||||||
FileFormatConstants.GROUP_TYPE_SIMPLE
|
FileFormatConstants.GROUP_TYPE_SIMPLE
|
||||||
);
|
);
|
||||||
vm.AddGroup(group);
|
vm.AddGroup(group);
|
||||||
@@ -304,6 +306,76 @@ public partial class RepositoryWindow : Window
|
|||||||
_contextMenuEntry = null;
|
_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)
|
private async void EntryContextMenuItem_Click(object sender, RoutedEventArgs args)
|
||||||
{
|
{
|
||||||
if (args.Source is StyledElement s && s.DataContext is PassStoreEntry ent)
|
if (args.Source is StyledElement s && s.DataContext is PassStoreEntry ent)
|
||||||
|
|||||||
Reference in New Issue
Block a user