mirror of
https://github.com/KeyKeeperApp/KeyKeeper.git
synced 2026-05-19 06:46:32 +03:00
374 lines
12 KiB
C#
374 lines
12 KiB
C#
using System;
|
|
using System.Linq;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Input;
|
|
using KeyKeeper.PasswordStore;
|
|
using KeyKeeper.ViewModels;
|
|
using Avalonia.VisualTree;
|
|
using Avalonia.Controls.Presenters;
|
|
|
|
namespace KeyKeeper.Views;
|
|
|
|
public partial class RepositoryWindow : Window
|
|
{
|
|
private bool allowClose;
|
|
private bool closeConfirmationShown;
|
|
private PassStoreEntry? _contextMenuEntry;
|
|
|
|
public RepositoryWindow(RepositoryWindowViewModel model)
|
|
{
|
|
InitializeComponent();
|
|
|
|
MinWidth = 650;
|
|
MinHeight = 500;
|
|
|
|
DataContext = model;
|
|
model.ShowErrorPopup = async (string message) =>
|
|
{
|
|
await new ErrorDialog(message).ShowDialog(this);
|
|
};
|
|
}
|
|
|
|
protected override void OnOpened(EventArgs e)
|
|
{
|
|
base.OnOpened(e);
|
|
AddHandler(PointerMovedEvent, OnUserActivity, RoutingStrategies.Tunnel);
|
|
AddHandler(PointerPressedEvent, OnUserActivity, RoutingStrategies.Tunnel);
|
|
AddHandler(KeyDownEvent, OnUserActivity, RoutingStrategies.Tunnel);
|
|
}
|
|
|
|
protected override void OnClosed(EventArgs e)
|
|
{
|
|
// Stop TOTP refresh timer when window closes
|
|
if (DataContext is RepositoryWindowViewModel vm &&
|
|
vm.CurrentPage is UnlockedRepositoryViewModel unlockedVm)
|
|
{
|
|
unlockedVm.StopTotpRefreshTimer();
|
|
}
|
|
base.OnClosed(e);
|
|
}
|
|
|
|
private void OnUserActivity(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is RepositoryWindowViewModel vm)
|
|
vm.ResetLockTimer();
|
|
}
|
|
|
|
private void UnlockPasswordEdit_Loaded(object? sender, RoutedEventArgs e)
|
|
{
|
|
(sender as TextBox)?.Focus();
|
|
}
|
|
|
|
private async void RepositoryWindow_Closing(object? sender, WindowClosingEventArgs e)
|
|
{
|
|
if (allowClose || closeConfirmationShown)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (DataContext is RepositoryWindowViewModel checkVm)
|
|
{
|
|
if ((checkVm.CurrentPage is UnlockedRepositoryViewModel unlockedVm &&
|
|
!unlockedVm.HasUnsavedChanges)
|
|
|| checkVm.CurrentPage is LockedRepositoryViewModel)
|
|
{
|
|
allowClose = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
e.Cancel = true;
|
|
closeConfirmationShown = true;
|
|
|
|
var dialog = new CloseConfirmationDialog();
|
|
var result = await dialog.ShowDialog<CloseConfirmationResult?>(this);
|
|
|
|
closeConfirmationShown = false;
|
|
|
|
if (result == null || result == CloseConfirmationResult.Cancel)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (result == CloseConfirmationResult.Save &&
|
|
DataContext is RepositoryWindowViewModel vm &&
|
|
vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
|
|
{
|
|
pageVm.Save();
|
|
}
|
|
|
|
allowClose = true;
|
|
Close();
|
|
}
|
|
|
|
private async void AddEntryButton_Click(object sender, RoutedEventArgs args)
|
|
{
|
|
if (DataContext is RepositoryWindowViewModel vm_ && vm_.CurrentPage is UnlockedRepositoryViewModel vm)
|
|
{
|
|
EntryEditWindow dialog = new();
|
|
|
|
vm_.StopLockTimer();
|
|
|
|
await dialog.ShowDialog(this);
|
|
|
|
vm_.StartLockTimer();
|
|
|
|
if (dialog.EditedEntry != null)
|
|
vm.AddEntry(dialog.EditedEntry);
|
|
}
|
|
}
|
|
|
|
private async void EditEntry(RepositoryWindowViewModel vm1, UnlockedRepositoryViewModel vm2, PassStoreEntry entry)
|
|
{
|
|
if (entry == null)
|
|
{
|
|
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("No entry selected");
|
|
return;
|
|
}
|
|
|
|
EntryEditWindow dialog = new();
|
|
|
|
PassStoreEntry realEntry = entry;
|
|
if (realEntry is PassStoreEntryLink lnk)
|
|
realEntry = lnk.LinkTarget!;
|
|
|
|
dialog.SetEntry((realEntry as PassStoreEntryPassword)!);
|
|
|
|
vm1.StopLockTimer();
|
|
|
|
await dialog.ShowDialog(this);
|
|
|
|
vm1.StartLockTimer();
|
|
|
|
if (dialog.EditedEntry != null)
|
|
{
|
|
if (entry is PassStoreEntryLink l)
|
|
l.LinkTarget = dialog.EditedEntry;
|
|
vm2.UpdateEntry(dialog.EditedEntry);
|
|
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Entry updated");
|
|
}
|
|
}
|
|
|
|
private async void EditEntryButton_Click(object sender, RoutedEventArgs args)
|
|
{
|
|
if (DataContext is RepositoryWindowViewModel vm_ && vm_.CurrentPage is UnlockedRepositoryViewModel vm)
|
|
{
|
|
var listBox = this.FindControlRecursive<ListBox>("PasswordsListBox");
|
|
if (listBox == null)
|
|
{
|
|
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("ListBox not found");
|
|
return;
|
|
}
|
|
|
|
PassStoreEntry? entry = listBox.SelectedItem as PassStoreEntry;
|
|
if (entry == null) return;
|
|
EditEntry(vm_, vm, entry);
|
|
}
|
|
}
|
|
|
|
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<ToastNotificationHost>("NotificationHost")?.Show("Group created");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SaveButton_Click(object sender, RoutedEventArgs args)
|
|
{
|
|
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
|
|
{
|
|
pageVm.Save();
|
|
}
|
|
}
|
|
|
|
private void Entry_DoubleTapped(object sender, TappedEventArgs args)
|
|
{
|
|
if (args.Source is StyledElement s && s.DataContext is PassStoreEntryPassword pwd)
|
|
{
|
|
Clipboard!.SetTextAsync(pwd.Password.Value);
|
|
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Password copied to clipboard");
|
|
}
|
|
}
|
|
|
|
private void PasswordsListBox_KeyDown(object sender, KeyEventArgs args)
|
|
{
|
|
if (args.Key == Key.C && args.KeyModifiers == KeyModifiers.Control)
|
|
{
|
|
if (sender is ListBox list && list.SelectedItem is PassStoreEntryPassword pwd)
|
|
{
|
|
Clipboard!.SetTextAsync(pwd.Password.Value);
|
|
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Password copied to clipboard");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void EntryContextMenu_Opening(object? sender, RoutedEventArgs args)
|
|
{
|
|
if (sender is not ContextMenu contextMenu || DataContext is not RepositoryWindowViewModel vm ||
|
|
vm.CurrentPage is not UnlockedRepositoryViewModel pageVm)
|
|
return;
|
|
|
|
_contextMenuEntry = null;
|
|
|
|
if (contextMenu.Parent?.Parent is Border border && border.DataContext is PassStoreEntry entry)
|
|
{
|
|
_contextMenuEntry = entry;
|
|
}
|
|
|
|
var addToGroupItem = contextMenu.Items
|
|
.OfType<MenuItem>()
|
|
.FirstOrDefault(m => m.Name == "entryCtxMenuAddToGroup");
|
|
|
|
var removeFromGroupItem = contextMenu.Items
|
|
.OfType<MenuItem>()
|
|
.FirstOrDefault(m => m.Name == "entryCtxMenuRemoveFromGroup");
|
|
|
|
var isNonDefaultGroup = pageVm.SelectedPasswordGroup.GroupType != FileFormatConstants.GROUP_TYPE_DEFAULT;
|
|
if (removeFromGroupItem != null)
|
|
{
|
|
removeFromGroupItem.IsVisible = isNonDefaultGroup;
|
|
}
|
|
|
|
if (addToGroupItem == null)
|
|
return;
|
|
|
|
addToGroupItem.Items.Clear();
|
|
|
|
var nonDefaultGroups = pageVm.PasswordGroups
|
|
.Where(g => g.GroupType != FileFormatConstants.GROUP_TYPE_DEFAULT)
|
|
.ToList();
|
|
|
|
EventHandler<RoutedEventArgs> onSubmenuClick = (sender, args) => AddToGroup_Click(sender, args, _contextMenuEntry!);
|
|
foreach (var group in nonDefaultGroups)
|
|
{
|
|
var menuItem = new MenuItem
|
|
{
|
|
Header = group.DisplayName,
|
|
Tag = group
|
|
};
|
|
menuItem.Click += onSubmenuClick;
|
|
addToGroupItem.Items.Add(menuItem);
|
|
}
|
|
}
|
|
|
|
private void AddToGroup_Click(object? sender, RoutedEventArgs args, PassStoreEntry entry)
|
|
{
|
|
if (sender is not MenuItem item || item.Tag is not PassStoreEntryGroup targetGroup)
|
|
return;
|
|
|
|
if (entry == null)
|
|
return;
|
|
|
|
if (DataContext is not RepositoryWindowViewModel vm ||
|
|
vm.CurrentPage is not UnlockedRepositoryViewModel pageVm)
|
|
return;
|
|
|
|
var notificationHost = this.FindControlRecursive<ToastNotificationHost>("NotificationHost");
|
|
|
|
if (pageVm.AddEntryToGroup(entry, targetGroup))
|
|
notificationHost?.Show($"Added to {targetGroup.DisplayName}");
|
|
else
|
|
notificationHost?.Show($"This entry is already in {targetGroup.DisplayName}!");
|
|
_contextMenuEntry = null;
|
|
}
|
|
|
|
private async void EntryContextMenuItem_Click(object sender, RoutedEventArgs args)
|
|
{
|
|
if (args.Source is StyledElement s && s.DataContext is PassStoreEntry ent)
|
|
{
|
|
PassStoreEntryPassword? pwd = UnlockedRepositoryViewModel.FollowLinkIfNeeded(ent);
|
|
if (pwd == null) return;
|
|
|
|
if (s.Name == "entryCtxMenuCopyUsername")
|
|
{
|
|
await Clipboard!.SetTextAsync(pwd.Username.Value);
|
|
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Username copied to clipboard");
|
|
}
|
|
else if (s.Name == "entryCtxMenuCopyPassword")
|
|
{
|
|
await Clipboard!.SetTextAsync(pwd.Password.Value);
|
|
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Password copied to clipboard");
|
|
}
|
|
else if (s.Name == "entryCtxMenuEdit")
|
|
{
|
|
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
|
|
{
|
|
EditEntry(vm, pageVm, ent);
|
|
}
|
|
}
|
|
else if (s.Name == "entryCtxMenuRemoveFromGroup")
|
|
{
|
|
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
|
|
{
|
|
pageVm.RemoveEntryFromGroup(ent);
|
|
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Removed from group");
|
|
}
|
|
}
|
|
else if (s.Name == "entryCtxMenuDelete")
|
|
{
|
|
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
|
|
{
|
|
pageVm.DeleteEntry(ent.Id);
|
|
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Entry deleted");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class VisualTreeExtensions
|
|
{
|
|
public static T? FindControlRecursive<T>(this Visual parent, string name) where T : Visual
|
|
{
|
|
return FindControlRecursiveInternal<T>(parent, name, 0);
|
|
}
|
|
|
|
private static T? FindControlRecursiveInternal<T>(Visual parent, string name, int depth) where T : Visual
|
|
{
|
|
if (parent == null) return null;
|
|
if (parent is T t && parent.Name == name) return t;
|
|
|
|
foreach (var child in parent.GetVisualChildren())
|
|
{
|
|
if (child == null) continue;
|
|
var result = FindControlRecursiveInternal<T>(child, name, depth + 1);
|
|
if (result != null) return result;
|
|
}
|
|
|
|
if (parent is ContentPresenter contentPresenter)
|
|
{
|
|
var content = contentPresenter.Content as Visual;
|
|
if (content != null)
|
|
{
|
|
var result = FindControlRecursiveInternal<T>(content, name, depth + 1);
|
|
if (result != null) return result;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|