mirror of
https://github.com/KeyKeeperApp/KeyKeeper.git
synced 2026-04-21 15:06:29 +03:00
implement entry deletion and toast notifications
1. add ToastNotificationHost custom control to display popup messages in the center of the window 2. add IPassStoreDirectory.DeleteEntry() method and implement it for PassStoreEntryGroup 3. make the Delete context menu option delete the selected entry and notify the user
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace KeyKeeper.PasswordStore;
|
||||
|
||||
public interface IPassStoreDirectory : IEnumerable<PassStoreEntry>
|
||||
{
|
||||
bool DeleteEntry(Guid id);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace KeyKeeper.PasswordStore;
|
||||
public abstract class PassStoreEntry
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public PassStoreEntryGroup? Parent { get; set; }
|
||||
public DateTime CreationDate { get; protected set; }
|
||||
public DateTime ModificationDate { get; set; }
|
||||
public Guid IconType { get; set; }
|
||||
|
||||
@@ -45,22 +45,40 @@ public class PassStoreEntryGroup : PassStoreEntry, IPassStoreDirectory
|
||||
customGroupSubtype = new Guid(guidBuffer);
|
||||
}
|
||||
|
||||
PassStoreEntryGroup group = new(id, createdAt, modifiedAt, iconType, name, groupType, null, customGroupSubtype);
|
||||
|
||||
int entryCount = rd.Read7BitEncodedInt();
|
||||
List<PassStoreEntry> children = new();
|
||||
for (int i = 0; i < entryCount; i++)
|
||||
children.Add(PassStoreEntry.ReadFromStream(str));
|
||||
{
|
||||
PassStoreEntry entry = PassStoreEntry.ReadFromStream(str);
|
||||
entry.Parent = group;
|
||||
children.Add(entry);
|
||||
}
|
||||
group.ChildEntries = children;
|
||||
|
||||
return new PassStoreEntryGroup(
|
||||
id, createdAt, modifiedAt,
|
||||
iconType, name, groupType, children,
|
||||
customGroupSubtype
|
||||
);
|
||||
return group;
|
||||
} catch (EndOfStreamException)
|
||||
{
|
||||
throw PassStoreFileException.UnexpectedEndOfFile;
|
||||
}
|
||||
}
|
||||
|
||||
public bool DeleteEntry(Guid id)
|
||||
{
|
||||
if (ChildEntries == null)
|
||||
return false;
|
||||
for (int i = 0; i < ChildEntries.Count; i++)
|
||||
{
|
||||
if (ChildEntries[i].Id == id)
|
||||
{
|
||||
ChildEntries.RemoveAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
IEnumerator<PassStoreEntry> IEnumerable<PassStoreEntry>.GetEnumerator()
|
||||
{
|
||||
return ChildEntries.GetEnumerator();
|
||||
|
||||
@@ -33,6 +33,12 @@ public class UnlockedRepositoryViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteEntry(Guid id)
|
||||
{
|
||||
(passStore.GetRootDirectory() as PassStoreEntryGroup)!.DeleteEntry(id);
|
||||
OnPropertyChanged(nameof(Passwords));
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
passStore.Save();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:KeyKeeper.ViewModels"
|
||||
xmlns:i="using:Avalonia.Interactivity"
|
||||
xmlns:kkp="using:KeyKeeper.Views"
|
||||
x:Class="KeyKeeper.Views.RepositoryWindow"
|
||||
Title="KeyKeeper - Password store"
|
||||
CanResize="False"
|
||||
@@ -93,6 +94,7 @@
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<kkp:ToastNotificationHost x:Name="NotificationHost" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20" Duration="0:0:2" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:LockedRepositoryViewModel}">
|
||||
|
||||
@@ -5,6 +5,8 @@ using Avalonia.Interactivity;
|
||||
using Avalonia.Input;
|
||||
using KeyKeeper.PasswordStore;
|
||||
using KeyKeeper.ViewModels;
|
||||
using Avalonia.VisualTree;
|
||||
using Avalonia.Controls.Presenters;
|
||||
|
||||
namespace KeyKeeper.Views;
|
||||
|
||||
@@ -50,7 +52,10 @@ public partial class RepositoryWindow: Window
|
||||
if (args.Source is StyledElement s)
|
||||
{
|
||||
if (s.DataContext is PassStoreEntryPassword pwd)
|
||||
{
|
||||
Clipboard!.SetTextAsync(pwd.Password.Value);
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Password copied to clipboard");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,16 +67,66 @@ public partial class RepositoryWindow: Window
|
||||
if (s.Name == "entryCtxMenuCopyUsername")
|
||||
{
|
||||
Clipboard!.SetTextAsync(pwd.Username.Value);
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Username copied to clipboard");
|
||||
}
|
||||
else if (s.Name == "entryCtxMenuCopyPassword")
|
||||
{
|
||||
Clipboard!.SetTextAsync(pwd.Password.Value);
|
||||
this.FindControlRecursive<ToastNotificationHost>("NotificationHost")?.Show("Password copied to clipboard");
|
||||
}
|
||||
else if (s.Name == "entryCtxMenuDelete")
|
||||
{
|
||||
Console.WriteLine("DELETE");
|
||||
if (s.DataContext is PassStoreEntryPassword entry)
|
||||
{
|
||||
if (DataContext is RepositoryWindowViewModel vm && vm.CurrentPage is UnlockedRepositoryViewModel pageVm)
|
||||
{
|
||||
pageVm.DeleteEntry(entry.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;
|
||||
}
|
||||
|
||||
// Also check logical children if they're not in visual tree
|
||||
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;
|
||||
}
|
||||
}
|
||||
15
src/KeyKeeper/Views/ToastNotificationHost.axaml
Normal file
15
src/KeyKeeper/Views/ToastNotificationHost.axaml
Normal file
@@ -0,0 +1,15 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:KeyKeeper.Views"
|
||||
x:Class="KeyKeeper.Views.ToastNotificationHost"
|
||||
Opacity="0">
|
||||
|
||||
<Border Background="#aa666666"
|
||||
CornerRadius="18"
|
||||
Padding="16 12"
|
||||
IsHitTestVisible="False">
|
||||
<TextBlock Text="{Binding Message, RelativeSource={RelativeSource AncestorType=local:ToastNotificationHost}}"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="White"/>
|
||||
</Border>
|
||||
</UserControl>
|
||||
97
src/KeyKeeper/Views/ToastNotificationHost.axaml.cs
Normal file
97
src/KeyKeeper/Views/ToastNotificationHost.axaml.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace KeyKeeper.Views
|
||||
{
|
||||
public partial class ToastNotificationHost : UserControl
|
||||
{
|
||||
private CancellationTokenSource? _hideCancellation;
|
||||
private Animation? _fadeOutAnimation;
|
||||
|
||||
public static readonly StyledProperty<string> MessageProperty =
|
||||
AvaloniaProperty.Register<ToastNotificationHost, string>(nameof(Message), string.Empty);
|
||||
|
||||
public static readonly StyledProperty<TimeSpan> DurationProperty =
|
||||
AvaloniaProperty.Register<ToastNotificationHost, TimeSpan>(nameof(Duration), TimeSpan.FromSeconds(5));
|
||||
|
||||
public string Message
|
||||
{
|
||||
get => GetValue(MessageProperty);
|
||||
set => SetValue(MessageProperty, value);
|
||||
}
|
||||
|
||||
public TimeSpan Duration
|
||||
{
|
||||
get => GetValue(DurationProperty);
|
||||
set => SetValue(DurationProperty, value);
|
||||
}
|
||||
|
||||
public ToastNotificationHost()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_fadeOutAnimation = new Animation
|
||||
{
|
||||
Duration = TimeSpan.FromSeconds(0.5),
|
||||
FillMode = FillMode.Forward,
|
||||
Children =
|
||||
{
|
||||
new KeyFrame
|
||||
{
|
||||
Cue = new Cue(0d),
|
||||
Setters = { new Setter(OpacityProperty, 1.0) },
|
||||
},
|
||||
new KeyFrame
|
||||
{
|
||||
Cue = new Cue(1d),
|
||||
Setters = { new Setter(OpacityProperty, 0.0) },
|
||||
}
|
||||
}
|
||||
};
|
||||
Opacity = 0;
|
||||
}
|
||||
|
||||
public void Show(string message)
|
||||
{
|
||||
_hideCancellation?.Cancel();
|
||||
_hideCancellation = new CancellationTokenSource();
|
||||
Message = message;
|
||||
Opacity = 1;
|
||||
|
||||
_ = HideAfterDelay(_hideCancellation.Token);
|
||||
}
|
||||
|
||||
private void FadeOut()
|
||||
{
|
||||
_fadeOutAnimation?.RunAsync(this);
|
||||
}
|
||||
|
||||
private async Task HideAfterDelay(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(Duration, cancellationToken);
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
FadeOut();
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// caught
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_hideCancellation?.Cancel();
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user