mirror of
https://github.com/KeyKeeperApp/KeyKeeper.git
synced 2026-04-06 12:52:09 +03:00
merge branch 'feature/password-context-menu'
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
bin/
|
||||
obj/
|
||||
.vs/
|
||||
*.kkp
|
||||
|
||||
@@ -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"
|
||||
@@ -82,10 +83,18 @@
|
||||
Foreground="#666"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Border.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Name="entryCtxMenuCopyUsername" Header="Copy username" Click="EntryContextMenuItem_Click"/>
|
||||
<MenuItem Name="entryCtxMenuCopyPassword" Header="Copy password" Click="EntryContextMenuItem_Click"/>
|
||||
<MenuItem Name="entryCtxMenuDelete" Header="Delete" Click="EntryContextMenuItem_Click"/>
|
||||
</ContextMenu>
|
||||
</Border.ContextMenu>
|
||||
</Border>
|
||||
</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}">
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
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;
|
||||
|
||||
@@ -51,7 +52,81 @@ 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EntryContextMenuItem_Click(object sender, RoutedEventArgs args) {
|
||||
if (args.Source is StyledElement s)
|
||||
{
|
||||
if (s.DataContext is PassStoreEntryPassword pwd)
|
||||
{
|
||||
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")
|
||||
{
|
||||
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